mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-23 01:27:07 +00:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c448bbc3b4 | |||
| 4dcf26236e | |||
| a0e8d69243 | |||
| 33cd9bf516 | |||
| 0132797f2f | |||
| 7e2db0aa4e | |||
| cc1d50491b | |||
| 461b86a04b | |||
| 9a3a7c28f4 | |||
| 1c9d0200ca | |||
| 0037cd2e69 | |||
| 028ae061da | |||
| 2e47763ecc | |||
| 924e4a45d0 | |||
| 8361b9553b | |||
| e52be20fba | |||
| da12233ade | |||
| 57500f6c97 | |||
| 199e17d0b1 | |||
| 3b91343082 | |||
| 1753c9163c | |||
| 89e5953e89 | |||
| 5bfd25c8c6 | |||
| e555b24f50 | |||
| 14db37459f | |||
| 1d9779d47c | |||
| 3dedbc5457 | |||
| facb3266c6 | |||
| 01fe5dd2f7 | |||
| 9b22b1e499 | |||
| 4bc8818145 | |||
| 48ef8c86c2 | |||
| 89f359a52f | |||
| 13b8160d74 | |||
| f1c16d6674 | |||
| 76a088e177 | |||
| 630d8c3bb6 | |||
| f0e959319e | |||
| d0c4475724 | |||
| 99935f1e59 | |||
| fbb43821ba | |||
| c7f5c6c1d1 | |||
| d26f1fa371 | |||
| c3718ff7dd | |||
| d63493a859 | |||
| a72183851a | |||
| 40b2387667 | |||
| d814aa36a7 | |||
| 58a58906e7 | |||
| ba99d1a10d | |||
| efe97e8f51 | |||
| 5ec23bb7ab | |||
| 9b4d01ab75 | |||
| 40191a88d4 | |||
| a19477d179 | |||
| bf98a78f3d | |||
| ba4c2fc1bd | |||
| b56e9ef028 | |||
| dbbd34c520 | |||
| ccb69dbdfa | |||
| 11e555ef6f | |||
| 61e17395c9 | |||
| 733ce3b6b8 | |||
| 375f143199 | |||
| 2419f35eb9 | |||
| 21867c3576 | |||
| 28853b28bc | |||
| e2f27568a5 | |||
| 98b2b796b0 | |||
| b8f3fcf00b | |||
| d3fda9a821 | |||
| 19e69dc13e | |||
| 5fa7cd9fa9 | |||
| a78c00fb41 | |||
| edc2a03d1c | |||
| 174f8f5823 | |||
| 9fbc94e8d8 | |||
| 6aff35196d | |||
| eceed4ed74 | |||
| 7428731eac | |||
| 89b07ea0ae | |||
| d16daf0fd9 | |||
| 211ab4eea8 | |||
| dbd53f8d14 |
@@ -55,7 +55,6 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
!latestBuild && [
|
||||
require("@babel/preset-env").default,
|
||||
{
|
||||
modules: false,
|
||||
useBuiltIns: "entry",
|
||||
corejs: "3.6",
|
||||
},
|
||||
@@ -71,7 +70,6 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/plugin-syntax-import-meta",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-syntax-top-level-await",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
|
||||
@@ -7,7 +7,6 @@ const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
const { mapFiles } = require("../util");
|
||||
@@ -301,7 +300,6 @@ gulp.task("build-flattened-translations", function () {
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
|
||||
@@ -51,9 +51,6 @@ const createWebpackConfig = ({
|
||||
}),
|
||||
],
|
||||
},
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
},
|
||||
plugins: [
|
||||
new ManifestPlugin({
|
||||
// Only include the JS of entrypoints
|
||||
@@ -98,6 +95,15 @@ const createWebpackConfig = ({
|
||||
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
require.resolve(
|
||||
"lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
||||
)
|
||||
),
|
||||
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
|
||||
),
|
||||
],
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
|
||||
@@ -7,205 +7,183 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
cards: [
|
||||
{ type: "custom:ha-demo-card" },
|
||||
{
|
||||
type: "grid",
|
||||
columns: 4,
|
||||
cards: [
|
||||
{
|
||||
cards: [
|
||||
image: "/assets/teachingbirds/isa_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_isa",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/Stefan_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_stefan",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
image: "/assets/teachingbirds/isa_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
state_image: {
|
||||
on: "/assets/teachingbirds/radiator_on.jpg",
|
||||
off: "/assets/teachingbirds/radiator_off.jpg",
|
||||
},
|
||||
type: "image",
|
||||
style: {
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_isa",
|
||||
entity: "switch.stefan_radiator_3",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/Stefan_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
style: {
|
||||
top: "90%",
|
||||
left: "50%",
|
||||
},
|
||||
entity: "sensor.presence_stefan",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
state_image: {
|
||||
on: "/assets/teachingbirds/radiator_on.jpg",
|
||||
off: "/assets/teachingbirds/radiator_off.jpg",
|
||||
},
|
||||
type: "image",
|
||||
style: {
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "switch.stefan_radiator_3",
|
||||
},
|
||||
{
|
||||
style: {
|
||||
top: "90%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "state-label",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
],
|
||||
type: "picture-elements",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
style: {
|
||||
"--mdc-icon-size": "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "icon",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/lovelace/home_info",
|
||||
},
|
||||
icon: "mdi:car",
|
||||
},
|
||||
],
|
||||
type: "picture-elements",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
name: "Alarm",
|
||||
image: "/assets/teachingbirds/House_square.jpg",
|
||||
entity: "alarm_control_panel.house",
|
||||
},
|
||||
{
|
||||
name: "Roomba",
|
||||
image: "/assets/teachingbirds/roomba_square.jpg",
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
|
||||
},
|
||||
entity: "input_select.roomba_mode",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
Mail: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
"Package and mail":
|
||||
"/assets/teachingbirds/mailbox_square.jpg",
|
||||
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
|
||||
Package: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
},
|
||||
entity: "sensor.mailbox",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
state_image: {
|
||||
"Put out": "/assets/teachingbirds/trash_square.jpg",
|
||||
"Take in": "/assets/teachingbirds/trash_square.jpg",
|
||||
},
|
||||
type: "picture-entity",
|
||||
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
|
||||
entity: "sensor.trash_status",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/washer_square.jpg",
|
||||
Running: "/assets/teachingbirds/laundry_running_square.jpg",
|
||||
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
|
||||
},
|
||||
entity: "input_select.washing_machine_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Washer",
|
||||
},
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/dryer_square.jpg",
|
||||
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
|
||||
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
|
||||
},
|
||||
entity: "input_select.dryer_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Dryer",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/guests_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.guest_mode",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/cleaning_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.cleaning_day",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
],
|
||||
type: "vertical-stack",
|
||||
},
|
||||
{
|
||||
type: "vertical-stack",
|
||||
cards: [
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_bedroom",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "S's room",
|
||||
type: "state-label",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
type: "picture-elements",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_passage",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "Laundry",
|
||||
entity: "sensor.temperature_downstairs_bathroom",
|
||||
style: {
|
||||
"--mdc-icon-size": "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "icon",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/lovelace/home_info",
|
||||
},
|
||||
icon: "mdi:car",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
type: "picture-elements",
|
||||
},
|
||||
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
name: "Alarm",
|
||||
image: "/assets/teachingbirds/House_square.jpg",
|
||||
entity: "alarm_control_panel.house",
|
||||
},
|
||||
{
|
||||
name: "Roomba",
|
||||
image: "/assets/teachingbirds/roomba_square.jpg",
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
|
||||
},
|
||||
entity: "input_select.roomba_mode",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
Mail: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
"Package and mail": "/assets/teachingbirds/mailbox_square.jpg",
|
||||
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
|
||||
Package: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
},
|
||||
entity: "sensor.mailbox",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
state_image: {
|
||||
"Put out": "/assets/teachingbirds/trash_square.jpg",
|
||||
"Take in": "/assets/teachingbirds/trash_square.jpg",
|
||||
},
|
||||
type: "picture-entity",
|
||||
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
|
||||
entity: "sensor.trash_status",
|
||||
},
|
||||
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/washer_square.jpg",
|
||||
Running: "/assets/teachingbirds/laundry_running_square.jpg",
|
||||
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
|
||||
},
|
||||
entity: "input_select.washing_machine_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Washer",
|
||||
},
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/dryer_square.jpg",
|
||||
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
|
||||
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
|
||||
},
|
||||
entity: "input_select.dryer_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Dryer",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/guests_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.guest_mode",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/cleaning_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.cleaning_day",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_bedroom",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "S's room",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_passage",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "Laundry",
|
||||
entity: "sensor.temperature_downstairs_bathroom",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,4 +6,11 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
body: { message: "Template dev tool does not work in the demo." },
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, onChange) => {
|
||||
onChange!({
|
||||
result: msg.template,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
return () => {};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,11 +5,16 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "./demo-card";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
|
||||
class DemoCards extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#container {
|
||||
min-height: calc(100vh - 128px);
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -24,6 +29,9 @@ class DemoCards extends PolymerElement {
|
||||
.filters {
|
||||
margin-left: 60px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
@@ -31,16 +39,21 @@ class DemoCards extends PolymerElement {
|
||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
config="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-card>
|
||||
</template>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
config="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -59,6 +72,12 @@ class DemoCards extends PolymerElement {
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-cards", DemoCards);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
import "./more-info-content";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
|
||||
class DemoMoreInfo extends PolymerElement {
|
||||
static get template() {
|
||||
@@ -16,15 +16,12 @@ class DemoMoreInfo extends PolymerElement {
|
||||
|
||||
ha-card {
|
||||
width: 333px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
state-card-content {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
more-info-content {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
|
||||
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
|
||||
import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-automation";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-camera";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-climate";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-configurator";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-counter";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-cover";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-default";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-fan";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-group";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-light";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-lock";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-media_player";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-person";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-script";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-sun";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-timer";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-weather";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
class MoreInfoContent extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
private _detachedChild?: ChildNode;
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.style.position = "relative";
|
||||
this.style.display = "block";
|
||||
}
|
||||
|
||||
// This is not a lit element, but an updating element, so we implement update
|
||||
protected update(changedProps: PropertyValues): void {
|
||||
super.update(changedProps);
|
||||
const stateObj = this.stateObj;
|
||||
const hass = this.hass;
|
||||
|
||||
if (!stateObj || !hass) {
|
||||
if (this.lastChild) {
|
||||
this._detachedChild = this.lastChild;
|
||||
// Detach child to prevent it from doing work.
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._detachedChild) {
|
||||
this.appendChild(this._detachedChild);
|
||||
this._detachedChild = undefined;
|
||||
}
|
||||
|
||||
const moreInfoType =
|
||||
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
|
||||
? stateObj.attributes.custom_ui_more_info
|
||||
: "more-info-" + stateMoreInfoType(stateObj);
|
||||
|
||||
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
|
||||
hass,
|
||||
stateObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-content", MoreInfoContent);
|
||||
@@ -15,6 +15,10 @@ const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "unavailable", "unavailable", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("alarm_control_panel", "alarm_code", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
code_format: "number",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -30,7 +34,14 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: alarm-panel
|
||||
entity: alarm_control_panel.alarm_armed
|
||||
title: My Alarm
|
||||
name: My Alarm
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Code Example",
|
||||
config: `
|
||||
- type: alarm-panel
|
||||
entity: alarm_control_panel.alarm_code
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -83,8 +94,12 @@ class DemoAlarmPanelEntity extends PolymerElement {
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.updateTranslations(null, "en");
|
||||
await hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-button-card", DemoButtonEntity);
|
||||
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../components/demo-cards";
|
||||
const ENTITIES = [
|
||||
getEntity("sensor", "brightness", "12", {}),
|
||||
getEntity("plant", "bonsai", "ok", {}),
|
||||
getEntity("sensor", "not_working", "unavailable", {}),
|
||||
getEntity("sensor", "outside_humidity", "54", {
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
@@ -74,6 +75,13 @@ const CONFIGS = [
|
||||
entity: plant.bonsai
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Unavailable entity",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.not_working
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoGaugeEntity extends PolymerElement {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { mockTemplate } from "../../../demo/src/stubs/template";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -254,7 +256,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoMarkdown extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards configs="[[_configs]]"></demo-cards> `;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -265,6 +267,12 @@ class DemoMarkdown extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
mockTemplate(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-markdown-card", DemoMarkdown);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { mockHistory } from "../../../demo/src/stubs/history";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -36,6 +37,10 @@ const ENTITIES = [
|
||||
battery: 71,
|
||||
friendly_name: "Home Boy",
|
||||
}),
|
||||
getEntity("sensor", "illumination", "23", {
|
||||
friendly_name: "Illumination",
|
||||
unit_of_measurement: "lx",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -89,6 +94,42 @@ const CONFIGS = [
|
||||
entity: light.bed_light
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Default Grid",
|
||||
config: `
|
||||
- type: grid
|
||||
cards:
|
||||
- type: entity
|
||||
entity: light.kitchen_lights
|
||||
- type: entity
|
||||
entity: light.bed_light
|
||||
- type: entity
|
||||
entity: device_tracker.demo_paulus
|
||||
- type: sensor
|
||||
entity: sensor.illumination
|
||||
graph: line
|
||||
- type: entity
|
||||
entity: device_tracker.demo_anne_therese
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Non-square Grid with 2 columns",
|
||||
config: `
|
||||
- type: grid
|
||||
columns: 2
|
||||
square: false
|
||||
cards:
|
||||
- type: entity
|
||||
entity: light.kitchen_lights
|
||||
- type: entity
|
||||
entity: light.bed_light
|
||||
- type: entity
|
||||
entity: device_tracker.demo_paulus
|
||||
- type: sensor
|
||||
entity: sensor.illumination
|
||||
graph: line
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoStack extends PolymerElement {
|
||||
@@ -110,6 +151,7 @@ class DemoStack extends PolymerElement {
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockHistory(hass);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-more-infos";
|
||||
import "../components/more-info-content";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "bed_light", "on", {
|
||||
@@ -40,8 +40,12 @@ class DemoMoreInfoLight extends PolymerElement {
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
await hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
createHassioSession,
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@@ -35,6 +38,17 @@ class HassioIngressView extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public narrow = false;
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
@@ -83,10 +97,7 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
const createSessionPromise = createHassioSession(this.hass);
|
||||
|
||||
let addon;
|
||||
|
||||
@@ -119,7 +130,11 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
let session;
|
||||
|
||||
try {
|
||||
session = await createSessionPromise;
|
||||
} catch (err) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
@@ -128,6 +143,17 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
this._sessionKeepAlive = window.setInterval(async () => {
|
||||
try {
|
||||
await validateHassioSession(this.hass, session);
|
||||
} catch (err) {
|
||||
session = await createHassioSession(this.hass);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -119,6 +119,7 @@
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sortablejs": "^1.10.2",
|
||||
"superstruct": "^0.10.12",
|
||||
"tinykeys": "^1.1.1",
|
||||
"unfetch": "^4.1.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
@@ -139,7 +140,6 @@
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-syntax-top-level-await": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
@@ -177,7 +177,6 @@
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"src/panels/iframe/ha-panel-iframe.js",
|
||||
"src/panels/logbook/ha-panel-logbook.js",
|
||||
"src/panels/map/ha-panel-map.js",
|
||||
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||
"hassio/src/entrypoint.js"
|
||||
],
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
const expand_hex = (hex: string): string => {
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
import { expandHex } from "./hex";
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
@@ -14,10 +8,7 @@ const rgb_hex = (component: number): string => {
|
||||
// Conversion between HEX and RGB
|
||||
|
||||
export const hex2rgb = (hex: string): [number, number, number] => {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
hex = expand_hex(hex);
|
||||
}
|
||||
hex = expandHex(hex);
|
||||
|
||||
return [
|
||||
parseInt(hex.substring(0, 2), 16),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const expandHex = (hex: string): string => {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 6) return hex;
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
|
||||
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
|
||||
let color = "";
|
||||
c1 = expandHex(c1);
|
||||
c2 = expandHex(c2);
|
||||
for (let i = 0; i <= 5; i += 2) {
|
||||
const h1 = parseInt(c1.substr(i, 2), 16);
|
||||
const h2 = parseInt(c2.substr(i, 2), 16);
|
||||
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
|
||||
while (hex.length < 2) hex = "0" + hex;
|
||||
color += hex;
|
||||
}
|
||||
return `#${color}`;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
} from "../color/convert-color";
|
||||
import { hexBlend } from "../color/hex";
|
||||
import { labBrighten, labDarken } from "../color/lab";
|
||||
import { rgbContrast } from "../color/rgb";
|
||||
|
||||
@@ -37,6 +38,13 @@ export const applyThemesOnElement = (
|
||||
if (themeOptions.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = darkStyles;
|
||||
if (themeOptions.primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
themeOptions.primaryColor,
|
||||
"#121212",
|
||||
8
|
||||
);
|
||||
}
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { numberFormat } from "../string/number-format";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -19,7 +20,9 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${compareState} ${stateObj.attributes.unit_of_measurement}`;
|
||||
return `${numberFormat(compareState, language)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
@@ -43,6 +43,7 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
}
|
||||
case "blind":
|
||||
case "curtain":
|
||||
case "shade":
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
|
||||
@@ -59,7 +59,7 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
: fuzzySequentialMatch(filter, item.text);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score === undefined || item.score > 0)
|
||||
.filter((item) => item.score !== undefined && item.score > 0)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const numberFormat = (
|
||||
num: string | number,
|
||||
language: string
|
||||
): string => {
|
||||
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(language).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
@@ -13,9 +13,12 @@ export interface FormatsType {
|
||||
time: FormatType;
|
||||
}
|
||||
|
||||
if (shouldPolyfill()) {
|
||||
await import("@formatjs/intl-pluralrules/polyfill-locales");
|
||||
}
|
||||
let polyfillLoaded = !shouldPolyfill();
|
||||
const polyfillProm = polyfillLoaded
|
||||
? undefined
|
||||
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
|
||||
polyfillLoaded = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Adapted from Polymer app-localize-behavior.
|
||||
@@ -38,12 +41,16 @@ if (shouldPolyfill()) {
|
||||
* }
|
||||
*/
|
||||
|
||||
export const computeLocalize = (
|
||||
export const computeLocalize = async (
|
||||
cache: any,
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): LocalizeFunc => {
|
||||
): Promise<LocalizeFunc> => {
|
||||
if (!polyfillLoaded) {
|
||||
await polyfillProm;
|
||||
}
|
||||
|
||||
// Everytime any of the parameters change, invalidate the strings cache.
|
||||
cache._localizationCache = {};
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
.chartTooltip .beforeBody {
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
word-break: break-all;
|
||||
}
|
||||
.chartLegend li {
|
||||
display: inline-block;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import "../ha-icon";
|
||||
|
||||
class HaStateIcon extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <ha-icon icon="[[computeIcon(stateObj)]]"></ha-icon> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeIcon(stateObj) {
|
||||
return stateIcon(stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-state-icon", HaStateIcon);
|
||||
@@ -59,6 +59,19 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: no-wrap;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin: 0 2px 4px 0;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
@@ -81,11 +94,26 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
datetime="[[stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
<paper-tooltip animation-delay="0" for="last_changed">
|
||||
[[localize('ui.dialogs.more_info_control.last_updated')]]:
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_updated]]"
|
||||
></ha-relative-time>
|
||||
<div>
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
[[localize('ui.dialogs.more_info_control.last_changed')]]:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
[[localize('ui.dialogs.more_info_control.last_updated')]]:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_updated]]"
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,7 +34,7 @@ class HaAttributes extends LitElement {
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${attribute.replace(/_/g, " ").replace("id", "ID")}
|
||||
${attribute.replace(/_/g, " ").replace(/\bid\b/g, "ID")}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.formatAttribute(attribute)}
|
||||
@@ -63,13 +63,14 @@ class HaAttributes extends LitElement {
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
.key:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
}
|
||||
pre {
|
||||
font-family: inherit;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="target">
|
||||
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
|
||||
<span class="state-label">
|
||||
[[_localizeState(localize, stateObj)]]
|
||||
<template is="dom-if" if="[[_renderPreset(stateObj.attributes)]]">
|
||||
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<div class="unit">[[computeTarget(hass, stateObj)]]</div>
|
||||
</div>
|
||||
|
||||
<template is="dom-if" if="[[currentStatus]]">
|
||||
<div class="current">
|
||||
[[localize('ui.card.climate.currently')]]:
|
||||
<div class="unit">[[currentStatus]]</div>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
currentStatus: {
|
||||
type: String,
|
||||
computed: "computeCurrentStatus(hass, stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeCurrentStatus(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
if (stateObj.attributes.current_temperature != null) {
|
||||
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.current_humidity != null) {
|
||||
return `${stateObj.attributes.current_humidity} %`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeTarget(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
if (
|
||||
stateObj.attributes.target_temp_low != null &&
|
||||
stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
stateObj.attributes.target_humidity_low != null &&
|
||||
stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
if (stateObj.attributes.humidity != null) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_hasKnownState(state) {
|
||||
return state !== "unknown";
|
||||
}
|
||||
|
||||
_localizeState(localize, stateObj) {
|
||||
const stateString = localize(`component.climate.state._.${stateObj.state}`);
|
||||
return stateObj.attributes.hvac_action
|
||||
? `${localize(
|
||||
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
_localizePreset(localize, preset) {
|
||||
return localize(`state_attributes.climate.preset_mode.${preset}`) || preset;
|
||||
}
|
||||
|
||||
_renderPreset(attributes) {
|
||||
return (
|
||||
attributes.preset_mode && attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-climate-state", HaClimateState);
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
class HaClimateState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
|
||||
return html`<div class="target">
|
||||
${this.stateObj.state !== "unknown"
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this.hass.localize(
|
||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||
) || this.stateObj.attributes.preset_mode}`
|
||||
: ""}
|
||||
</span>`
|
||||
: ""}
|
||||
<div class="unit">${this._computeTarget()}</div>
|
||||
</div>
|
||||
|
||||
${currentStatus
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${this.stateObj.attributes.current_humidity} %`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _computeTarget(): string {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${this.stateObj.attributes.humidity} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
const stateString = this.hass.localize(
|
||||
`component.climate.state._.${this.stateObj.state}`
|
||||
);
|
||||
|
||||
return this.stateObj.attributes.hvac_action
|
||||
? `${this.hass.localize(
|
||||
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-climate-state": HaClimateState;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._blockKeyboardShortcuts();
|
||||
this._load();
|
||||
}
|
||||
|
||||
@@ -232,6 +233,10 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
this.codemirror!.on("changes", () => this._onChange());
|
||||
}
|
||||
|
||||
private _blockKeyboardShortcuts() {
|
||||
this.addEventListener("keydown", (ev) => ev.stopPropagation());
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
const newValue = this.value;
|
||||
if (newValue === this._value) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { getAppKey } from "../data/notify_html5";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import "./ha-switch";
|
||||
|
||||
export const pushSupported =
|
||||
@@ -88,7 +89,14 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
|
||||
browserName = "chrome";
|
||||
}
|
||||
|
||||
const name = prompt("What should this device be called ?");
|
||||
const name = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.input_label"
|
||||
),
|
||||
});
|
||||
if (name == null) {
|
||||
this.pushChecked = false;
|
||||
return;
|
||||
|
||||
+217
-186
@@ -202,195 +202,17 @@ class HaSidebar extends LitElement {
|
||||
private _sortable?;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
||||
if (!hass) {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
hass.panels,
|
||||
hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
: 0;
|
||||
for (const entityId in hass.states) {
|
||||
if (computeDomain(entityId) === "configurator") {
|
||||
notificationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<div
|
||||
class="menu"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode,
|
||||
disabled: this.editMode,
|
||||
})}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@action=${this._toggleSidebar}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${hass.dockedSidebar === "docked"
|
||||
? mdiMenuOpen
|
||||
: mdiMenu}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="title">
|
||||
${this.editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
${hass.localize("ui.sidebar.done")}
|
||||
</mwc-button>`
|
||||
: "Home Assistant"}
|
||||
</div>
|
||||
</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${hass.panelUrl}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this.editMode
|
||||
? html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable
|
||||
? ""
|
||||
: this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>`
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
<div class="spacer" disabled></div>
|
||||
${this.editMode && this._hiddenPanels.length
|
||||
? html`
|
||||
${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
if (!panel) {
|
||||
return "";
|
||||
}
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
.panel=${url}
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === this.hass.defaultPanel
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === this.hass.defaultPanel
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<mwc-icon-button class="show-panel">
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
<div class="spacer" disabled></div>
|
||||
`
|
||||
: ""}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div
|
||||
class="notifications-container"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
aria-role="option"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge" slot="item-icon">
|
||||
${notificationCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">
|
||||
${hass.localize("ui.notification_drawer.title")}
|
||||
</span>
|
||||
${this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge">${notificationCount}</span>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=${classMap({
|
||||
profile: true,
|
||||
// Mimick behavior that paper-listbox provides
|
||||
"iron-selected": hass.panelUrl === "profile",
|
||||
})}
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
aria-role="option"
|
||||
aria-label=${hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${hass.user}
|
||||
.hass=${hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${hass.user ? hass.user.name : ""}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
${this._renderHeader()}
|
||||
${this._renderAllPanels()}
|
||||
${this._renderDivider()}
|
||||
${this._renderNotifications()}
|
||||
${this._renderUserItem()}
|
||||
<div disabled class="bottom-spacer"></div>
|
||||
<div class="tooltip"></div>
|
||||
`;
|
||||
@@ -475,6 +297,215 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHeader() {
|
||||
return html`<div
|
||||
class="menu"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode,
|
||||
disabled: this.editMode,
|
||||
})}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@action=${this._toggleSidebar}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.hass.dockedSidebar === "docked"
|
||||
? mdiMenuOpen
|
||||
: mdiMenu}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="title">
|
||||
${this.editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
${this.hass.localize("ui.sidebar.done")}
|
||||
</mwc-button>`
|
||||
: "Home Assistant"}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderAllPanels() {
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${this.hass.panelUrl}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this.editMode
|
||||
? this._renderPanelsEdit(beforeSpacer)
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
// prettier-ignore
|
||||
return html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>
|
||||
${this._renderSpacer()}
|
||||
${this._renderHiddenPanels()} `;
|
||||
}
|
||||
|
||||
private _renderHiddenPanels() {
|
||||
return html` ${this._hiddenPanels.length
|
||||
? html`${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
if (!panel) {
|
||||
return "";
|
||||
}
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
.panel=${url}
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === this.hass.defaultPanel
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === this.hass.defaultPanel
|
||||
? this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<mwc-icon-button class="show-panel">
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
${this._renderSpacer()}`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _renderDivider() {
|
||||
return html`<div class="divider"></div>`;
|
||||
}
|
||||
|
||||
private _renderSpacer() {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderNotifications() {
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
: 0;
|
||||
for (const entityId in this.hass.states) {
|
||||
if (computeDomain(entityId) === "configurator") {
|
||||
notificationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return html` <div
|
||||
class="notifications-container"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
aria-role="option"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge" slot="item-icon">
|
||||
${notificationCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.notification_drawer.title")}
|
||||
</span>
|
||||
${this.expanded && notificationCount > 0
|
||||
? html` <span class="notification-badge">${notificationCount}</span> `
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderUserItem() {
|
||||
return html`<a
|
||||
class=${classMap({
|
||||
profile: true,
|
||||
// Mimick behavior that paper-listbox provides
|
||||
"iron-selected": this.hass.panelUrl === "profile",
|
||||
})}
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -781,7 +812,7 @@ class HaSidebar extends LitElement {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
}
|
||||
#sortable,
|
||||
.hidden-panel {
|
||||
|
||||
@@ -540,17 +540,20 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
this._loading = true;
|
||||
const itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
this._loading = false;
|
||||
let itemData: any;
|
||||
try {
|
||||
itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
return itemData;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export const DISCOVERY_SOURCES = [
|
||||
"ssdp",
|
||||
"zeroconf",
|
||||
"discovery",
|
||||
"mqtt",
|
||||
];
|
||||
|
||||
export const ATTENTION_SOURCES = ["reauth"];
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HassioResponse } from "./common";
|
||||
import { CreateSessionResponse } from "./supervisor";
|
||||
|
||||
export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
||||
location.protocol === "https:" ? ";Secure" : ""
|
||||
}`;
|
||||
return response.data.session;
|
||||
};
|
||||
|
||||
export const validateHassioSession = async (
|
||||
hass: HomeAssistant,
|
||||
session: string
|
||||
) =>
|
||||
await hass.callApi<HassioResponse<null>>(
|
||||
"POST",
|
||||
"hassio/ingress/validate_session",
|
||||
{ session }
|
||||
);
|
||||
@@ -111,18 +111,6 @@ export const fetchHassioLogs = async (
|
||||
return hass.callApi<string>("GET", `hassio/${provider}/logs`);
|
||||
};
|
||||
|
||||
export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
||||
location.protocol === "https:" ? ";Secure" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
data: SupervisorOptions
|
||||
|
||||
@@ -89,6 +89,7 @@ export interface LovelaceViewConfig {
|
||||
export interface LovelaceViewElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
narrow?: boolean;
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const removeTasmotaDeviceEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "tasmota/device/remove",
|
||||
device_id: deviceId,
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -10,16 +9,16 @@ import {
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../components/dialog/ha-paper-dialog";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-formfield";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import {
|
||||
getConfigEntrySystemOptions,
|
||||
updateConfigEntrySystemOptions,
|
||||
} from "../../data/config_entries";
|
||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
||||
@@ -35,9 +34,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
|
||||
@internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams;
|
||||
|
||||
@internalProperty() private _loading?: boolean;
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _submitting?: boolean;
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: ConfigEntrySystemOptionsDialogParams
|
||||
@@ -51,7 +50,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
);
|
||||
this._loading = false;
|
||||
this._disableNewEntities = systemOptions.disable_new_entities;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -60,21 +64,17 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
"integration",
|
||||
this.hass.localize(`component.${this._params.entry.domain}.title`) ||
|
||||
this._params.entry.domain
|
||||
)}
|
||||
>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<div>
|
||||
${this._loading
|
||||
? html`
|
||||
<div class="init-spinner">
|
||||
@@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`}
|
||||
</paper-dialog-scrollable>
|
||||
${!this._loading
|
||||
? html`
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-paper-dialog>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting || this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -154,19 +154,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!(ev.detail as any).value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
.init-spinner {
|
||||
padding: 50px 100px;
|
||||
text-align: center;
|
||||
|
||||
@@ -64,6 +64,10 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
</ha-labeled-slider>
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="current_position,current_tilt_position"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,10 @@ import {
|
||||
DOMAINS_MORE_INFO_NO_HISTORY,
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
} from "../../common/const";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import {
|
||||
stateMoreInfoType,
|
||||
importMoreInfoControl,
|
||||
} from "./state_more_info_control";
|
||||
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-header-bar";
|
||||
@@ -38,6 +34,8 @@ import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import "./ha-more-info-history";
|
||||
import "./ha-more-info-logbook";
|
||||
import "./controls/more-info-default";
|
||||
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
|
||||
import "./more-info-content";
|
||||
|
||||
const DOMAINS_NO_INFO = ["camera", "configurator"];
|
||||
/**
|
||||
@@ -62,8 +60,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@internalProperty() private _entityId?: string | null;
|
||||
|
||||
@internalProperty() private _moreInfoType?: string;
|
||||
|
||||
@internalProperty() private _currTabIndex = 0;
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
@@ -73,18 +69,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
this.large = false;
|
||||
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
|
||||
this._moreInfoType = stateObj.attributes.custom_ui_more_info;
|
||||
} else {
|
||||
const type = stateMoreInfoType(stateObj);
|
||||
importMoreInfoControl(type);
|
||||
this._moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -169,7 +153,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
: ""}
|
||||
</ha-header-bar>
|
||||
${DOMAINS_WITH_MORE_INFO.includes(domain) &&
|
||||
this._computeShowHistoryComponent(entityId)
|
||||
(this._computeShowHistoryComponent(entityId) ||
|
||||
this._computeShowLogBookComponent(entityId))
|
||||
? html`
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._currTabIndex}
|
||||
@@ -206,19 +191,20 @@ export class MoreInfoDialog extends LitElement {
|
||||
!this._computeShowHistoryComponent(entityId)
|
||||
? ""
|
||||
: html`<ha-more-info-history
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history>
|
||||
<ha-more-info-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-logbook>`}
|
||||
${this._moreInfoType
|
||||
? dynamicElement(this._moreInfoType, {
|
||||
hass: this.hass,
|
||||
stateObj,
|
||||
})
|
||||
: ""}
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history>`}
|
||||
${DOMAINS_WITH_MORE_INFO.includes(domain) ||
|
||||
!this._computeShowLogBookComponent(entityId)
|
||||
? ""
|
||||
: html`<ha-more-info-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-logbook>`}
|
||||
<more-info-content
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></more-info-content>
|
||||
${stateObj.attributes.restored
|
||||
? html`
|
||||
<p>
|
||||
@@ -264,12 +250,32 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
private _computeShowHistoryComponent(entityId) {
|
||||
return (
|
||||
(isComponentLoaded(this.hass, "history") ||
|
||||
isComponentLoaded(this.hass, "logbook")) &&
|
||||
isComponentLoaded(this.hass, "history") &&
|
||||
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId))
|
||||
);
|
||||
}
|
||||
|
||||
private _computeShowLogBookComponent(entityId): boolean {
|
||||
if (!isComponentLoaded(this.hass, "logbook")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj || stateObj.attributes.unit_of_measurement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
if (
|
||||
CONTINUOUS_DOMAINS.includes(domain) ||
|
||||
DOMAINS_MORE_INFO_NO_HISTORY.includes(domain)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _removeEntity() {
|
||||
const entityId = this._entityId!;
|
||||
showConfirmationDialog(this, {
|
||||
|
||||
@@ -13,11 +13,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { throttle } from "../../common/util/throttle";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/state-history-charts";
|
||||
import {
|
||||
CONTINUOUS_DOMAINS,
|
||||
getLogbookData,
|
||||
LogbookEntry,
|
||||
} from "../../data/logbook";
|
||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -44,12 +40,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
}
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
|
||||
if (!stateObj || stateObj.attributes.unit_of_measurement) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
if (CONTINUOUS_DOMAINS.includes(domain)) {
|
||||
if (!stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import dynamicContentUpdater from "../../common/dom/dynamic_content_updater";
|
||||
import { stateMoreInfoType } from "./state_more_info_control";
|
||||
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
|
||||
|
||||
class MoreInfoContent extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
private _detachedChild?: ChildNode;
|
||||
|
||||
// This is not a lit element, but an updating element, so we implement update
|
||||
protected update(changedProps: PropertyValues): void {
|
||||
super.update(changedProps);
|
||||
const stateObj = this.stateObj;
|
||||
const hass = this.hass;
|
||||
|
||||
if (!stateObj || !hass) {
|
||||
if (this.lastChild) {
|
||||
this._detachedChild = this.lastChild;
|
||||
// Detach child to prevent it from doing work.
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._detachedChild) {
|
||||
this.appendChild(this._detachedChild);
|
||||
this._detachedChild = undefined;
|
||||
}
|
||||
|
||||
let moreInfoType: string | undefined;
|
||||
|
||||
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
|
||||
moreInfoType = stateObj.attributes.custom_ui_more_info;
|
||||
} else {
|
||||
const type = stateMoreInfoType(stateObj);
|
||||
importMoreInfoControl(type);
|
||||
moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
|
||||
}
|
||||
|
||||
if (!moreInfoType) {
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
|
||||
hass,
|
||||
stateObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-content", MoreInfoContent);
|
||||
@@ -36,6 +36,10 @@ import "../../components/ha-header-bar";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
ConfirmationDialogParams,
|
||||
showConfirmationDialog,
|
||||
} from "../generic/show-dialog-box";
|
||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
@@ -55,12 +59,12 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@internalProperty() private _filter = "";
|
||||
|
||||
@internalProperty() private _search = "";
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _commandMode = false;
|
||||
|
||||
@internalProperty() private _commandTriggered = -1;
|
||||
|
||||
@internalProperty() private _done = false;
|
||||
|
||||
@query("search-input", false) private _filterInputField?: HTMLElement;
|
||||
@@ -79,7 +83,7 @@ export class QuickBar extends LitElement {
|
||||
this._done = false;
|
||||
this._focusSet = false;
|
||||
this._filter = "";
|
||||
this._commandTriggered = -1;
|
||||
this._search = "";
|
||||
this._items = [];
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -116,7 +120,7 @@ export class QuickBar extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.filter=${this._commandMode ? `>${this._filter}` : this._filter}
|
||||
.filter=${this._commandMode ? `>${this._search}` : this._search}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
@@ -134,7 +138,6 @@ export class QuickBar extends LitElement {
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`<mwc-list
|
||||
activatable
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
@@ -189,19 +192,12 @@ export class QuickBar extends LitElement {
|
||||
<span slot="secondary" class="secondary">${item.altText}</span>
|
||||
`
|
||||
: null}
|
||||
${this._commandTriggered === index
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
slot="meta"
|
||||
></ha-circular-progress>`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||
this._commandTriggered = index;
|
||||
this._addSpinnerToCommandItem(index);
|
||||
|
||||
await item.action();
|
||||
this.closeDialog();
|
||||
@@ -231,18 +227,28 @@ export class QuickBar extends LitElement {
|
||||
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||
}
|
||||
|
||||
private _addSpinnerToCommandItem(index: number): void {
|
||||
const spinner = document.createElement("ha-circular-progress");
|
||||
spinner.size = "small";
|
||||
spinner.slot = "meta";
|
||||
spinner.active = true;
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
const newFilter = ev.detail.value;
|
||||
const oldCommandMode = this._commandMode;
|
||||
|
||||
if (newFilter.startsWith(">")) {
|
||||
this._commandMode = true;
|
||||
this._debouncedSetFilter(newFilter.substring(1));
|
||||
this._search = newFilter.substring(1);
|
||||
} else {
|
||||
this._commandMode = false;
|
||||
this._debouncedSetFilter(newFilter);
|
||||
this._search = newFilter;
|
||||
}
|
||||
|
||||
this._debouncedSetFilter(this._search);
|
||||
|
||||
if (oldCommandMode !== this._commandMode) {
|
||||
this._items = undefined;
|
||||
this._focusSet = false;
|
||||
@@ -274,9 +280,10 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _generateCommandItems(): QuickBarItem[] {
|
||||
return [...this._generateReloadCommands()].sort((a, b) =>
|
||||
compare(a.text.toLowerCase(), b.text.toLowerCase())
|
||||
);
|
||||
return [
|
||||
...this._generateReloadCommands(),
|
||||
...this._generateServerControlCommands(),
|
||||
].sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
|
||||
}
|
||||
|
||||
private _generateReloadCommands(): QuickBarItem[] {
|
||||
@@ -295,10 +302,45 @@ export class QuickBar extends LitElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private _generateServerControlCommands(): QuickBarItem[] {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) =>
|
||||
this._generateConfirmationCommand(
|
||||
{
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
),
|
||||
icon: "hass:server",
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _generateConfirmationCommand(
|
||||
item: QuickBarItem,
|
||||
confirmText: ConfirmationDialogParams["confirmText"]
|
||||
): QuickBarItem {
|
||||
return {
|
||||
...item,
|
||||
action: () =>
|
||||
showConfirmationDialog(this, {
|
||||
confirmText,
|
||||
confirm: item.action,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private _generateEntityItems(): QuickBarItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => ({
|
||||
text: computeStateName(this.hass.states[entityId]),
|
||||
text: computeStateName(this.hass.states[entityId]) || entityId,
|
||||
altText: entityId,
|
||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
|
||||
@@ -53,19 +53,21 @@ export const provideHass = (
|
||||
} = {};
|
||||
const entities = {};
|
||||
|
||||
function updateTranslations(fragment: null | string, language?: string) {
|
||||
async function updateTranslations(
|
||||
fragment: null | string,
|
||||
language?: string
|
||||
) {
|
||||
const lang = language || getLocalLanguage();
|
||||
getTranslation(fragment, lang).then((translation) => {
|
||||
const resources = {
|
||||
[lang]: {
|
||||
...(hass().resources && hass().resources[lang]),
|
||||
...translation.data,
|
||||
},
|
||||
};
|
||||
hass().updateHass({
|
||||
resources,
|
||||
localize: computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
const translation = await getTranslation(fragment, lang);
|
||||
const resources = {
|
||||
[lang]: {
|
||||
...(hass().resources && hass().resources[lang]),
|
||||
...translation.data,
|
||||
},
|
||||
};
|
||||
hass().updateHass({
|
||||
resources,
|
||||
localize: await computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class NotificationManager extends LitElement {
|
||||
|
||||
@internalProperty() private _noCancelOnOutsideClick = false;
|
||||
|
||||
@query("ha-toast", true) private _toast!: HaToast;
|
||||
@query("ha-toast") private _toast!: HaToast;
|
||||
|
||||
public async showDialog({
|
||||
message,
|
||||
|
||||
@@ -36,11 +36,13 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
(changedProperties.has("language") ||
|
||||
changedProperties.has("resources"))
|
||||
) {
|
||||
this.localize = computeLocalize(
|
||||
computeLocalize(
|
||||
this.constructor.prototype,
|
||||
this.language,
|
||||
this.resources
|
||||
);
|
||||
).then((localize) => {
|
||||
this.localize = localize;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fullcalendarStyle from "@fullcalendar/common/main.css";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
// @ts-ignore
|
||||
import daygridStyle from "@fullcalendar/daygrid/main.css";
|
||||
@@ -44,6 +45,15 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const getListWeekRange = (currentDate: Date): { start: Date; end: Date } => {
|
||||
const startDate = new Date(currentDate.valueOf());
|
||||
const endDate = new Date(currentDate.valueOf());
|
||||
|
||||
endDate.setDate(endDate.getDate() + 7);
|
||||
|
||||
return { start: startDate, end: endDate };
|
||||
};
|
||||
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
headerToolbar: false,
|
||||
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
|
||||
@@ -51,16 +61,22 @@ const defaultFullCalendarConfig: CalendarOptions = {
|
||||
dayMaxEventRows: true,
|
||||
height: "parent",
|
||||
eventDisplay: "list-item",
|
||||
locales: allLocales,
|
||||
views: {
|
||||
list: {
|
||||
visibleRange: getListWeekRange,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const viewButtons: ToggleButton[] = [
|
||||
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
|
||||
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
|
||||
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
|
||||
{ label: "List View", value: "listWeek", iconPath: mdiViewAgenda },
|
||||
{ label: "List View", value: "list", iconPath: mdiViewAgenda },
|
||||
];
|
||||
|
||||
class HAFullCalendar extends LitElement {
|
||||
export class HAFullCalendar extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@@ -79,6 +95,10 @@ class HAFullCalendar extends LitElement {
|
||||
|
||||
@internalProperty() private _activeView?: FullCalendarView;
|
||||
|
||||
public updateSize(): void {
|
||||
this.calendar?.updateSize();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const viewToggleButtons = this._viewToggleButtons(this.views);
|
||||
|
||||
@@ -186,6 +206,12 @@ class HAFullCalendar extends LitElement {
|
||||
this.calendar!.changeView(this._activeView);
|
||||
this._fireViewChanged();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
|
||||
if (oldHass && oldHass.language !== this.hass.language) {
|
||||
this.calendar.setOption("locale", this.hass.language);
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
@@ -243,7 +269,7 @@ class HAFullCalendar extends LitElement {
|
||||
this._fireViewChanged();
|
||||
}
|
||||
|
||||
private _handleView(ev): void {
|
||||
private _handleView(ev: CustomEvent): void {
|
||||
this._activeView = ev.detail.value;
|
||||
this.calendar!.changeView(this._activeView!);
|
||||
this._fireViewChanged();
|
||||
@@ -324,10 +350,19 @@ class HAFullCalendar extends LitElement {
|
||||
|
||||
#calendar {
|
||||
flex-grow: 1;
|
||||
background-color: var(--card-background-color);
|
||||
background-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
min-height: 400px;
|
||||
--fc-neutral-bg-color: var(--card-background-color);
|
||||
--fc-list-event-hover-bg-color: var(--card-background-color);
|
||||
--fc-neutral-bg-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
--fc-list-event-hover-bg-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
--fc-border-color: var(--divider-color);
|
||||
}
|
||||
@@ -487,6 +522,23 @@ class HAFullCalendar extends LitElement {
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fc-scroller::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.fc-scroller::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
.fc-scroller {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
CSSResultArray,
|
||||
css,
|
||||
TemplateResult,
|
||||
html,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@material/mwc-checkbox";
|
||||
import "@material/mwc-formfield";
|
||||
|
||||
import "../../components/ha-menu-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-card";
|
||||
import "./ha-full-calendar";
|
||||
|
||||
import "../../components/ha-menu-button";
|
||||
import { fetchCalendarEvents, getCalendars } from "../../data/calendar";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
SelectedCalendar,
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarViewChanged,
|
||||
Calendar,
|
||||
HomeAssistant,
|
||||
} from "../../types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { getCalendars, fetchCalendarEvents } from "../../data/calendar";
|
||||
import "./ha-full-calendar";
|
||||
|
||||
@customElement("ha-panel-calendar")
|
||||
class PanelCalendar extends LitElement {
|
||||
@@ -39,20 +36,20 @@ class PanelCalendar extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
@internalProperty() private _calendars: SelectedCalendar[] = [];
|
||||
@internalProperty() private _calendars: Calendar[] = [];
|
||||
|
||||
@internalProperty() private _events: CalendarEvent[] = [];
|
||||
|
||||
@LocalStorage("deSelectedCalendars", true)
|
||||
private _deSelectedCalendars: string[] = [];
|
||||
|
||||
private _start?: Date;
|
||||
|
||||
private _end?: Date;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._calendars = getCalendars(this.hass).map((calendar) => ({
|
||||
selected: true,
|
||||
calendar,
|
||||
}));
|
||||
this._calendars = getCalendars(this.hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -78,19 +75,22 @@ class PanelCalendar extends LitElement {
|
||||
</div>
|
||||
${this._calendars.map(
|
||||
(selCal) =>
|
||||
html`<div>
|
||||
<mwc-formfield .label=${selCal.calendar.name}>
|
||||
<mwc-checkbox
|
||||
style=${styleMap({
|
||||
"--mdc-theme-secondary": selCal.calendar
|
||||
.backgroundColor!,
|
||||
})}
|
||||
.value=${selCal.calendar.entity_id}
|
||||
.checked=${selCal.selected}
|
||||
@change=${this._handleToggle}
|
||||
></mwc-checkbox>
|
||||
</mwc-formfield>
|
||||
</div>`
|
||||
html`
|
||||
<div>
|
||||
<mwc-formfield .label=${selCal.name}>
|
||||
<mwc-checkbox
|
||||
style=${styleMap({
|
||||
"--mdc-theme-secondary": selCal.backgroundColor!,
|
||||
})}
|
||||
.value=${selCal.entity_id}
|
||||
.checked=${!this._deSelectedCalendars.includes(
|
||||
selCal.entity_id
|
||||
)}
|
||||
@change=${this._handleToggle}
|
||||
></mwc-checkbox>
|
||||
</mwc-formfield>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-full-calendar
|
||||
@@ -106,8 +106,8 @@ class PanelCalendar extends LitElement {
|
||||
|
||||
private get _selectedCalendars(): Calendar[] {
|
||||
return this._calendars
|
||||
.filter((selCal) => selCal.selected)
|
||||
.map((cal) => cal.calendar);
|
||||
.filter((selCal) => !this._deSelectedCalendars.includes(selCal.entity_id))
|
||||
.map((cal) => cal);
|
||||
}
|
||||
|
||||
private async _fetchEvents(
|
||||
@@ -124,24 +124,28 @@ class PanelCalendar extends LitElement {
|
||||
|
||||
private async _handleToggle(ev): Promise<void> {
|
||||
const results = this._calendars.map(async (cal) => {
|
||||
if (ev.target.value !== cal.calendar.entity_id) {
|
||||
if (ev.target.value !== cal.entity_id) {
|
||||
return cal;
|
||||
}
|
||||
|
||||
const checked = ev.target.checked;
|
||||
|
||||
if (checked) {
|
||||
const events = await this._fetchEvents(this._start!, this._end!, [
|
||||
cal.calendar,
|
||||
]);
|
||||
const events = await this._fetchEvents(this._start!, this._end!, [cal]);
|
||||
this._events = [...this._events, ...events];
|
||||
this._deSelectedCalendars = this._deSelectedCalendars.filter(
|
||||
(deCal) => deCal !== cal.entity_id
|
||||
);
|
||||
} else {
|
||||
this._events = this._events.filter(
|
||||
(event) => event.calendar !== cal.calendar.entity_id
|
||||
(event) => event.calendar !== cal.entity_id
|
||||
);
|
||||
this._deSelectedCalendars = [
|
||||
...this._deSelectedCalendars,
|
||||
cal.entity_id,
|
||||
];
|
||||
}
|
||||
|
||||
cal.selected = checked;
|
||||
return cal;
|
||||
});
|
||||
|
||||
@@ -179,7 +183,7 @@ class PanelCalendar extends LitElement {
|
||||
}
|
||||
|
||||
:host(:not([narrow])) .content {
|
||||
height: calc(100vh - var(--header-height);
|
||||
height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
.calendar-list {
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../components/dialog/ha-paper-dialog";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dialog";
|
||||
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
@@ -37,24 +38,27 @@ class DialogAreaDetail extends LitElement {
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const entry = this._params.entry;
|
||||
const nameInvalid = this._name.trim() === "";
|
||||
const nameInvalid = !this._isNameValid();
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${entry
|
||||
? entry.name
|
||||
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
|
||||
>
|
||||
<h2>
|
||||
${entry
|
||||
? entry.name
|
||||
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="form">
|
||||
${entry
|
||||
@@ -71,6 +75,7 @@ class DialogAreaDetail extends LitElement {
|
||||
<paper-input
|
||||
.value=${this._name}
|
||||
@value-changed=${this._nameChanged}
|
||||
@keyup=${this._handleKeyup}
|
||||
.label=${this.hass.localize("ui.panel.config.areas.editor.name")}
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.name_required"
|
||||
@@ -78,32 +83,42 @@ class DialogAreaDetail extends LitElement {
|
||||
.invalid=${nameInvalid}
|
||||
></paper-input>
|
||||
</div>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
${entry
|
||||
? html`
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.areas.editor.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.panel.config.areas.editor.update")
|
||||
: this.hass.localize("ui.panel.config.areas.editor.create")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
${entry
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.areas.editor.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.panel.config.areas.editor.update")
|
||||
: this.hass.localize("ui.panel.config.areas.editor.create")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isNameValid() {
|
||||
return this._name.trim() !== "";
|
||||
}
|
||||
|
||||
private _handleKeyup(ev: KeyboardEvent) {
|
||||
if (ev.keyCode === 13 && this._isNameValid() && !this._submitting) {
|
||||
this._updateEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private _nameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._name = ev.detail.value;
|
||||
@@ -141,12 +156,6 @@ class DialogAreaDetail extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!(ev.detail as any).value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
@@ -154,9 +163,6 @@ class DialogAreaDetail extends LitElement {
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
mwc-button.warning {
|
||||
margin-right: auto;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@@ -126,9 +126,10 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
></ha-icon-button>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title="${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_area"
|
||||
)}"
|
||||
)}
|
||||
extended
|
||||
@click=${this._createArea}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -30,6 +30,9 @@ export const handleChangeEvent = (
|
||||
ev: CustomEvent
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
return;
|
||||
}
|
||||
const name = (ev.target as any)?.name;
|
||||
if (!name) {
|
||||
return;
|
||||
|
||||
@@ -41,14 +41,27 @@ export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
allow-custom-value
|
||||
></ha-entity-attribute-picker>
|
||||
<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.state.state"
|
||||
)}
|
||||
.name=${"state"}
|
||||
.value=${state}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
${Array.isArray(state)
|
||||
? html`
|
||||
<ha-yaml-editor
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.state.state"
|
||||
) + " (YAML)"}
|
||||
.defaultValue=${state}
|
||||
.name=${"state"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.state.state"
|
||||
)}
|
||||
.name=${"state"}
|
||||
.value=${state}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -472,7 +472,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
.title=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
|
||||
@@ -176,9 +176,10 @@ class HaAutomationPicker extends LitElement {
|
||||
</mwc-icon-button>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.add_automation"
|
||||
)}
|
||||
extended
|
||||
@click=${this._createNew}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -85,13 +85,22 @@ class DialogThingtalk extends LitElement {
|
||||
.opened=${this._opened}
|
||||
@opened-changed=${this._openedChanged}
|
||||
>
|
||||
<h2>Create a new automation</h2>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.header`
|
||||
)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
Type below what this automation should do, and we will try to convert
|
||||
it into a Home Assistant automation. (only English is supported for
|
||||
now)<br /><br />
|
||||
For example:
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.introduction`
|
||||
)}<br /><br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.language_note`
|
||||
)}<br /><br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.for_example`
|
||||
)}
|
||||
<ul @click=${this._handleExampleClick}>
|
||||
<li>
|
||||
<button class="link">
|
||||
@@ -130,7 +139,7 @@ class DialogThingtalk extends LitElement {
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button class="left" @click="${this._skip}">
|
||||
Skip
|
||||
${this.hass.localize(`ui.common.skip`)}
|
||||
</mwc-button>
|
||||
<mwc-button @click="${this._generate}" .disabled=${this._submitting}>
|
||||
${this._submitting
|
||||
@@ -140,7 +149,7 @@ class DialogThingtalk extends LitElement {
|
||||
title="Creating your automation..."
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
Create automation
|
||||
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
@@ -150,7 +159,9 @@ class DialogThingtalk extends LitElement {
|
||||
private async _generate() {
|
||||
this._value = this._input!.value as string;
|
||||
if (!this._value) {
|
||||
this._error = "Enter a command or tap skip.";
|
||||
this._error = this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.error_empty`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
@@ -169,7 +180,9 @@ class DialogThingtalk extends LitElement {
|
||||
this._submitting = false;
|
||||
|
||||
if (!Object.keys(config).length) {
|
||||
this._error = "We couldn't create an automation for that (yet?).";
|
||||
this._error = this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.error_unsupported`
|
||||
);
|
||||
} else if (Object.keys(placeholders).length) {
|
||||
this._config = config;
|
||||
this._placeholders = placeholders;
|
||||
|
||||
@@ -131,7 +131,11 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
.opened=${this.opened}
|
||||
@opened-changed="${this._openedChanged}"
|
||||
>
|
||||
<h2>Great! Now we need to link some devices.</h2>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.header`
|
||||
)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${Object.entries(this.placeholders).map(
|
||||
@@ -168,8 +172,9 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
${extraInfo && extraInfo.manualEntity
|
||||
? html`
|
||||
<h3>
|
||||
One or more devices have more than one matching
|
||||
entity, please pick the one you want to use.
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.ambiguous_entities`
|
||||
)}
|
||||
</h3>
|
||||
${Object.keys(extraInfo.manualEntity).map(
|
||||
(idx) => html`
|
||||
@@ -226,7 +231,9 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
return html`
|
||||
<div class="error">
|
||||
Unknown placeholder<br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.unknown_placeholder`
|
||||
)}<br />
|
||||
${placeholder.domains}<br />
|
||||
${placeholder.fields.map(
|
||||
(field) => html` ${field}<br /> `
|
||||
@@ -239,10 +246,10 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button class="left" @click="${this.skip}">
|
||||
Skip
|
||||
${this.hass.localize(`ui.common.skip`)}
|
||||
</mwc-button>
|
||||
<mwc-button @click="${this._done}" .disabled=${!this._isDone}>
|
||||
Create automation
|
||||
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import { removeTasmotaDeviceEntry } from "../../../../../../data/tasmota";
|
||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
@customElement("ha-device-actions-tasmota")
|
||||
export class HaDeviceActionsTasmota extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-button class="warning" @click="${this._confirmDeleteEntry}">
|
||||
${this.hass.localize("ui.panel.config.devices.delete")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _confirmDeleteEntry(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeTasmotaDeviceEntry(this.hass!, this.device.id);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -528,6 +528,19 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
if (integrations.includes("tasmota")) {
|
||||
import(
|
||||
"./device-detail/integration-elements/tasmota/ha-device-actions-tasmota"
|
||||
);
|
||||
templates.push(html`
|
||||
<div class="card-actions" slot="actions">
|
||||
<ha-device-actions-tasmota
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-actions-tasmota>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
if (integrations.includes("zha")) {
|
||||
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
|
||||
import("./device-detail/integration-elements/zha/ha-device-info-zha");
|
||||
|
||||
@@ -160,9 +160,10 @@ export class HaConfigHelpers extends LitElement {
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title="${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.add_helper"
|
||||
)}"
|
||||
)}
|
||||
extended
|
||||
@click=${this._createHelpler}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -72,7 +72,7 @@ class SystemHealthCard extends LitElement {
|
||||
}
|
||||
if (domain !== "homeassistant") {
|
||||
sections.push(
|
||||
html` <h3>${domainToName(this.hass.localize, domain)}</h3> `
|
||||
html`<h3>${domainToName(this.hass.localize, domain)}</h3>`
|
||||
);
|
||||
}
|
||||
sections.push(html`
|
||||
@@ -128,13 +128,17 @@ class SystemHealthCard extends LitElement {
|
||||
}
|
||||
|
||||
private _copyInfo(): void {
|
||||
const selection = window.getSelection()!;
|
||||
selection.removeAllRanges();
|
||||
|
||||
const copyElement = this.shadowRoot?.querySelector(
|
||||
"ha-card"
|
||||
".card-content"
|
||||
) as HTMLElement;
|
||||
|
||||
// Add temporary heading (fixed in EN since usually executed to provide support data)
|
||||
const tempTitle = document.createElement("h3");
|
||||
tempTitle.innerText = "System Health";
|
||||
copyElement.insertBefore(tempTitle, copyElement.firstElementChild);
|
||||
|
||||
const selection = window.getSelection()!;
|
||||
selection.removeAllRanges();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(copyElement);
|
||||
selection.addRange(range);
|
||||
@@ -142,6 +146,9 @@ class SystemHealthCard extends LitElement {
|
||||
document.execCommand("copy");
|
||||
window.getSelection()!.removeAllRanges();
|
||||
|
||||
// Remove temporary heading again
|
||||
copyElement.removeChild(tempTitle);
|
||||
|
||||
this._toolTip!.show();
|
||||
setTimeout(() => this._toolTip?.hide(), 3000);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import "../../../common/search/search-input";
|
||||
import { caseInsensitiveCompare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/entity/ha-state-icon";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -476,8 +475,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
|
||||
title=${this.hass.localize("ui.panel.config.integrations.new")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.add_integration"
|
||||
)}
|
||||
extended
|
||||
@click=${this._createFlow}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
@@ -739,6 +740,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -129,6 +129,7 @@ class OZWNetworkDashboard extends LitElement {
|
||||
<div class="card-actions">
|
||||
${this._generateServiceButton("add_node")}
|
||||
${this._generateServiceButton("remove_node")}
|
||||
${this._generateServiceButton("cancel_command")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
DataTableColumnContainer,
|
||||
HaDataTable,
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/entity/ha-state-icon";
|
||||
import type { Cluster } from "../../../../../data/zha";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
|
||||
@@ -84,7 +84,8 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</ha-card>
|
||||
<a href="/config/zha/add" slot="fab">
|
||||
<mwc-fab
|
||||
title=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
extended
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
-1
@@ -15,7 +15,6 @@ import type {
|
||||
HaDataTable,
|
||||
DataTableRowData,
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/entity/ha-state-icon";
|
||||
import type {
|
||||
ZHADeviceEndpoint,
|
||||
ZHAEntityReference,
|
||||
|
||||
@@ -128,7 +128,10 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
>
|
||||
<a href="/config/zha/group-add" slot="fab">
|
||||
<mwc-fab
|
||||
title=${this.hass!.localize("ui.panel.config.zha.groups.add_group")}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.add_group"
|
||||
)}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
|
||||
@@ -225,9 +225,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title="${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.add_dashboard"
|
||||
)}"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addDashboard}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -105,9 +105,10 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.add_resource"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addResource}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -148,7 +148,8 @@ class HaConfigPerson extends LitElement {
|
||||
</ha-config-section>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title="${hass.localize("ui.panel.config.person.add_person")}"
|
||||
.label=${hass.localize("ui.panel.config.person.add_person")}
|
||||
extended
|
||||
@click=${this._createPerson}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -153,9 +153,10 @@ class HaSceneDashboard extends LitElement {
|
||||
</mwc-icon-button>
|
||||
<a href="/config/scene/edit/new" slot="fab">
|
||||
<mwc-fab
|
||||
title=${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.add_scene"
|
||||
)}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
|
||||
@@ -405,7 +405,8 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
</div>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
.title=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
||||
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
||||
extended
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
>
|
||||
|
||||
@@ -367,9 +367,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
</div>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
.title=${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.script.editor.save_script"
|
||||
)}
|
||||
extended
|
||||
@click=${this._saveScript}
|
||||
class=${classMap({
|
||||
dirty: this._dirty,
|
||||
|
||||
@@ -149,9 +149,10 @@ class HaScriptPicker extends LitElement {
|
||||
<mwc-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.create_new_script"
|
||||
)}"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.script.picker.add_script"
|
||||
)}
|
||||
extended
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -231,7 +231,7 @@ class DialogTagDetail extends LitElement
|
||||
private async _generateQR() {
|
||||
const qrcode = await import("qrcode");
|
||||
const canvas = await qrcode.toCanvas(
|
||||
`https://home-assistant.io/tag/${this._params?.entry?.id}`,
|
||||
`https://www.home-assistant.io/tag/${this._params!.entry!.id}`,
|
||||
{
|
||||
width: 180,
|
||||
errorCorrectionLevel: "Q",
|
||||
|
||||
@@ -209,7 +209,8 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
</mwc-icon-button>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize("ui.panel.config.tags.add_tag")}
|
||||
.label=${this.hass.localize("ui.panel.config.tags.add_tag")}
|
||||
extended
|
||||
@click=${this._addTag}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -100,7 +100,8 @@ export class HaConfigUsers extends LitElement {
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
.title=${this.hass.localize("ui.panel.config.users.picker.add_user")}
|
||||
.label=${this.hass.localize("ui.panel.config.users.picker.add_user")}
|
||||
extended
|
||||
@click=${this._addUser}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -257,7 +257,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${hass.localize("ui.panel.config.zone.add_zone")}
|
||||
.label=${hass.localize("ui.panel.config.zone.add_zone")}
|
||||
extended
|
||||
@click=${this._createZone}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -42,6 +42,9 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
|
||||
.entities th {
|
||||
text-align: left;
|
||||
font-size: var(
|
||||
--paper-input-container-shared-input-style_-_font-size
|
||||
);
|
||||
}
|
||||
|
||||
:host([rtl]) .entities th {
|
||||
@@ -62,7 +65,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
}
|
||||
.entities td {
|
||||
padding: 4px;
|
||||
min-width: 200px;
|
||||
min-width: 220px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.entities ha-svg-icon {
|
||||
@@ -168,7 +171,13 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
></ha-svg-icon>
|
||||
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
|
||||
</td>
|
||||
<td>[[entity.state]]</td>
|
||||
<td>
|
||||
[[entity.state]]<br /><br />
|
||||
<span class="secondary">
|
||||
last_changed: [[lastChangedString(entity)]]<br />
|
||||
last_updated: [[lastUpdatedString(entity)]]
|
||||
</span>
|
||||
</td>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[computeShowAttributes(narrow, _showAttributes)]]"
|
||||
@@ -381,6 +390,14 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
return output;
|
||||
}
|
||||
|
||||
lastChangedString(entity) {
|
||||
return new Date(entity.last_changed).toISOString();
|
||||
}
|
||||
|
||||
lastUpdatedString(entity) {
|
||||
return new Date(entity.last_updated).toISOString();
|
||||
}
|
||||
|
||||
formatAttributeValue(value) {
|
||||
if (
|
||||
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||
|
||||
@@ -82,6 +82,13 @@ class HaPanelDevTemplate extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = typeof this._templateResult?.result;
|
||||
const resultType =
|
||||
type === "object"
|
||||
? Array.isArray(this._templateResult?.result)
|
||||
? "list"
|
||||
: "dict"
|
||||
: type;
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
@@ -141,16 +148,28 @@ class HaPanelDevTemplate extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="render-pane">
|
||||
<ha-circular-progress
|
||||
class="render-spinner"
|
||||
.active=${this._rendering}
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
|
||||
${this._rendering
|
||||
? html`<ha-circular-progress
|
||||
class="render-spinner"
|
||||
active
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
${this._templateResult
|
||||
? html`${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.result_type"
|
||||
)}:
|
||||
${resultType}`
|
||||
: ""}
|
||||
<!-- prettier-ignore -->
|
||||
<pre
|
||||
class="rendered ${classMap({ error: Boolean(this._error) })}"
|
||||
><!-- display: block -->${this._error}${this._templateResult
|
||||
?.result}</pre>
|
||||
class="rendered ${classMap({
|
||||
error: Boolean(this._error),
|
||||
[resultType]: resultType,
|
||||
})}"
|
||||
>${this._error}${type === "object"
|
||||
? JSON.stringify(this._templateResult!.result, null, 2)
|
||||
: this._templateResult?.result}</pre>
|
||||
${this._templateResult?.listeners.time
|
||||
? html`
|
||||
<p>
|
||||
@@ -209,11 +228,13 @@ class HaPanelDevTemplate extends LitElement {
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: html` <span class="all_listeners">
|
||||
: !this._templateResult?.listeners.time
|
||||
? html` <span class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.no_listeners"
|
||||
)}
|
||||
</span>`}
|
||||
</span>`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HA_COLOR_PALETTE } from "../../../common/const";
|
||||
@@ -24,6 +25,7 @@ import type {
|
||||
HomeAssistant,
|
||||
} from "../../../types";
|
||||
import "../../calendar/ha-full-calendar";
|
||||
import type { HAFullCalendar } from "../../calendar/ha-full-calendar";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
import "../components/hui-warning";
|
||||
@@ -71,6 +73,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@internalProperty() private _veryNarrow = false;
|
||||
|
||||
@query("ha-full-calendar", true) private _calendar?: HAFullCalendar;
|
||||
|
||||
private _startDate?: Date;
|
||||
|
||||
private _endDate?: Date;
|
||||
@@ -121,8 +125,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const views: FullCalendarView[] = this._veryNarrow
|
||||
? ["listWeek"]
|
||||
: ["listWeek", "dayGridMonth", "dayGridDay"];
|
||||
? ["list"]
|
||||
: ["list", "dayGridMonth", "dayGridDay"];
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -186,6 +190,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
this._narrow = card.offsetWidth < 870;
|
||||
this._veryNarrow = card.offsetWidth < 350;
|
||||
|
||||
this._calendar?.updateSize();
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
|
||||
@@ -115,7 +115,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = entities;
|
||||
if (config.show_header_toggle === undefined) {
|
||||
if (config.title !== undefined && config.show_header_toggle === undefined) {
|
||||
// Default value is show toggle if we can at least toggle 2 entities.
|
||||
let toggleable = 0;
|
||||
for (const rowConf of entities) {
|
||||
|
||||
@@ -197,7 +197,6 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
display: flex;
|
||||
padding: 0 16px 4px;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { css, CSSResult } from "lit-element";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { HuiStackCard } from "./hui-stack-card";
|
||||
import { GridCardConfig } from "./types";
|
||||
|
||||
const DEFAULT_COLUMNS = 3;
|
||||
|
||||
class HuiGridCard extends HuiStackCard<GridCardConfig> {
|
||||
public async getCardSize(): Promise<number> {
|
||||
if (!this._cards || !this._config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.square) {
|
||||
// When we're square, each row is size 2.
|
||||
return (this._cards.length / this.columns) * 2;
|
||||
}
|
||||
|
||||
const promises: Array<Promise<number> | number> = [];
|
||||
|
||||
for (const element of this._cards) {
|
||||
promises.push(computeCardSize(element));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const maxCardSize = Math.max(...results);
|
||||
|
||||
return maxCardSize * (this._cards.length / this.columns);
|
||||
}
|
||||
|
||||
get columns() {
|
||||
return this._config?.columns || DEFAULT_COLUMNS;
|
||||
}
|
||||
|
||||
get square() {
|
||||
return this._config?.square !== false;
|
||||
}
|
||||
|
||||
setConfig(config: GridCardConfig) {
|
||||
super.setConfig(config);
|
||||
this.style.setProperty("--grid-card-column-count", String(this.columns));
|
||||
this.toggleAttribute("square", this.square);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
super.sharedStyles,
|
||||
css`
|
||||
#root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
var(--grid-card-column-count, ${DEFAULT_COLUMNS}),
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
grid-gap: var(--grid-card-gap, 8px);
|
||||
}
|
||||
:host([square]) #root {
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
:host([square]) #root::before {
|
||||
content: "";
|
||||
width: 0;
|
||||
padding-bottom: 100%;
|
||||
grid-row: 1 / 1;
|
||||
grid-column: 1 / 1;
|
||||
}
|
||||
|
||||
:host([square]) #root > *:first-child {
|
||||
grid-row: 1 / 1;
|
||||
grid-column: 1 / 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-grid-card": HuiGridCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-grid-card", HuiGridCard);
|
||||
@@ -189,7 +189,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
|
||||
@@ -14,7 +14,9 @@ import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { StackCardConfig } from "./types";
|
||||
|
||||
export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
extends LitElement
|
||||
implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import(
|
||||
/* webpackChunkName: "hui-stack-card-editor" */ "../editor/config-elements/hui-stack-card-editor"
|
||||
@@ -32,13 +34,13 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property() protected _cards?: LovelaceCard[];
|
||||
|
||||
@internalProperty() private _config?: StackCardConfig;
|
||||
@internalProperty() protected _config?: T;
|
||||
|
||||
public getCardSize(): number | Promise<number> {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public setConfig(config: StackCardConfig): void {
|
||||
public setConfig(config: T): void {
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
throw new Error("Card config incorrect");
|
||||
}
|
||||
@@ -87,6 +89,9 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
font-weight: normal;
|
||||
margin-block-start: 0px;
|
||||
margin-block-end: 0px;
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 32px;
|
||||
display: block;
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||
| "entity-id"
|
||||
| "last-changed"
|
||||
| "last-triggered"
|
||||
| "last-updated"
|
||||
| "position"
|
||||
| "tilt-position"
|
||||
| "brightness";
|
||||
@@ -287,6 +288,11 @@ export interface StackCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface GridCardConfig extends StackCardConfig {
|
||||
columns?: number;
|
||||
square?: boolean;
|
||||
}
|
||||
|
||||
export interface ThermostatCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
theme?: string;
|
||||
|
||||
@@ -97,6 +97,13 @@ class HuiGenericEntityRow extends LitElement {
|
||||
.datetime=${stateObj.last_changed}
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
|
||||
@@ -37,6 +37,7 @@ const LAZY_LOAD_TYPES = {
|
||||
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"empty-state": () => import("../cards/hui-empty-state-card"),
|
||||
grid: () => import("../cards/hui-grid-card"),
|
||||
starting: () => import("../cards/hui-starting-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
humidifier: () => import("../cards/hui-humidifier-card"),
|
||||
|
||||
@@ -54,6 +54,8 @@ export class HuiDialogEditCard extends LitElement
|
||||
implements HassDialog<EditCardDialogParams> {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@internalProperty() private _params?: EditCardDialogParams;
|
||||
|
||||
@internalProperty() private _cardConfig?: LovelaceCardConfig;
|
||||
@@ -82,6 +84,7 @@ export class HuiDialogEditCard extends LitElement
|
||||
this._viewConfig = params.lovelaceConfig.views[view];
|
||||
this._cardConfig =
|
||||
card !== undefined ? this._viewConfig.cards![card] : params.cardConfig;
|
||||
this.large = false;
|
||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||
this._cardConfig = deepFreeze(this._cardConfig);
|
||||
}
|
||||
@@ -162,7 +165,7 @@ export class HuiDialogEditCard extends LitElement
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<div slot="title">${heading}</div>
|
||||
<div slot="title" @click=${this._enlarge}>${heading}</div>
|
||||
${this._documentationURL !== undefined
|
||||
? html`
|
||||
<a
|
||||
@@ -254,6 +257,10 @@ export class HuiDialogEditCard extends LitElement
|
||||
`;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
@@ -374,6 +381,15 @@ export class HuiDialogEditCard extends LitElement
|
||||
--dialog-z-index: 5;
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
}
|
||||
:host([large]) .content {
|
||||
width: calc(90vw - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
|
||||
@@ -31,6 +31,7 @@ import { configElementStyle } from "./config-elements-style";
|
||||
const SecondaryInfoValues: { [key: string]: { domains?: string[] } } = {
|
||||
"entity-id": {},
|
||||
"last-changed": {},
|
||||
"last-updated": {},
|
||||
"last-triggered": { domains: ["automation", "script"] },
|
||||
position: { domains: ["cover"] },
|
||||
"tilt-position": { domains: ["cover"] },
|
||||
|
||||
@@ -91,6 +91,9 @@ export const coreCards: Card[] = [
|
||||
{
|
||||
type: "entity-filter",
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
},
|
||||
{
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
|
||||
@@ -110,6 +110,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
>
|
||||
<mwc-fab
|
||||
.label=${this.hass.localize("ui.panel.lovelace.editor.edit_card.add")}
|
||||
extended
|
||||
@click=${this._addToLovelaceView}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
|
||||
@@ -17,6 +17,7 @@ import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -381,7 +382,7 @@ class HUIRoot extends LitElement {
|
||||
${this._editMode
|
||||
? html`
|
||||
<div sticky>
|
||||
<ha-tabs
|
||||
<paper-tabs
|
||||
scrollable
|
||||
.selected="${this._curView}"
|
||||
@iron-activate="${this._handleViewSelected}"
|
||||
@@ -461,7 +462,7 @@ class HUIRoot extends LitElement {
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-tabs>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -490,6 +491,10 @@ class HUIRoot extends LitElement {
|
||||
huiView.hass = this.hass;
|
||||
}
|
||||
|
||||
if (changedProperties.has("narrow") && huiView) {
|
||||
huiView.narrow = this.narrow;
|
||||
}
|
||||
|
||||
let newSelectView;
|
||||
let force = false;
|
||||
|
||||
@@ -753,6 +758,7 @@ class HUIRoot extends LitElement {
|
||||
|
||||
view.lovelace = this.lovelace;
|
||||
view.hass = this.hass;
|
||||
view.narrow = this.narrow;
|
||||
|
||||
const configBackground = viewConfig.background || this.config.background;
|
||||
|
||||
@@ -784,19 +790,26 @@ class HUIRoot extends LitElement {
|
||||
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
background: var(--lovelace-background);
|
||||
background: var(
|
||||
--lovelace-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
}
|
||||
ha-tabs {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
margin-left: max(env(safe-area-inset-left), 12px);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
ha-tabs, paper-tabs {
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.edit-mode ha-tabs {
|
||||
margin-left: max(env(safe-area-inset-left), 24px);
|
||||
margin-right: max(env(safe-area-inset-right), 24px);
|
||||
}
|
||||
|
||||
.edit-mode {
|
||||
background-color: var(--dark-color, #455a64);
|
||||
color: var(--text-dark-color);
|
||||
|
||||
@@ -49,6 +49,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ attribute: false }) public cards: Array<
|
||||
@@ -82,9 +84,10 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
${this.lovelace?.editMode
|
||||
? html`
|
||||
<mwc-fab
|
||||
title=${this.hass!.localize(
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.add"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addCard}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass!),
|
||||
@@ -128,6 +131,10 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("narrow")) {
|
||||
this._updateColumns();
|
||||
}
|
||||
|
||||
const oldLovelace = changedProperties.get("lovelace") as
|
||||
| Lovelace
|
||||
| undefined;
|
||||
@@ -252,7 +259,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
// Do -1 column if the menu is docked and open
|
||||
this._columns = Math.max(
|
||||
1,
|
||||
matchColumns - Number(this.hass!.dockedSidebar === "docked")
|
||||
matchColumns -
|
||||
Number(!this.narrow && this.hass!.dockedSidebar === "docked")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (changedProperties.has("cards")) {
|
||||
this._createCard();
|
||||
}
|
||||
|
||||
if (!changedProperties.has("lovelace")) {
|
||||
return;
|
||||
}
|
||||
@@ -74,9 +78,10 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
${this.lovelace?.editMode && this.cards.length === 0
|
||||
? html`
|
||||
<mwc-fab
|
||||
title=${this.hass!.localize(
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.add"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addCard}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass!),
|
||||
|
||||
@@ -31,6 +31,8 @@ export class HUIView extends UpdatingElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@internalProperty() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
|
||||
@@ -111,6 +113,7 @@ export class HUIView extends UpdatingElement {
|
||||
this._createCards(viewConfig!);
|
||||
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
}
|
||||
@@ -127,6 +130,10 @@ export class HUIView extends UpdatingElement {
|
||||
this._layoutElement!.hass = this.hass;
|
||||
}
|
||||
|
||||
if (changedProperties.has("narrow")) {
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
}
|
||||
|
||||
if (editModeChanged) {
|
||||
this._layoutElement!.lovelace = lovelace;
|
||||
}
|
||||
@@ -217,7 +224,7 @@ export class HUIView extends UpdatingElement {
|
||||
badgeElToReplace
|
||||
);
|
||||
}
|
||||
this._badges = this._cards!.map((curBadgeEl) =>
|
||||
this._badges = this._badges!.map((curBadgeEl) =>
|
||||
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-settings-row";
|
||||
|
||||
@customElement("ha-enable-shortcuts-row")
|
||||
class HaEnableShortcutsRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.profile.enable_shortcuts.header")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize("ui.panel.profile.enable_shortcuts.description")}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this.hass.enableShortcuts}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _checkedChanged(ev: Event) {
|
||||
const enabled = (ev.target as HaSwitch).checked;
|
||||
if (enabled === this.hass.enableShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "hass-enable-shortcuts", enabled);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-enable-shortcuts-row": HaEnableShortcutsRow;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user