mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-22 15:49:26 +00:00
Compare commits
158 Commits
20201021.1
...
quick-bar-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0527fac2ce | ||
![]() |
75e5d66966 | ||
![]() |
b9f802939c | ||
![]() |
6558c2c065 | ||
![]() |
37a089c868 | ||
![]() |
f68eff6bb3 | ||
![]() |
88a525f1a7 | ||
![]() |
34fddd5940 | ||
![]() |
0e5d6fe8d8 | ||
![]() |
e1342a0d9d | ||
![]() |
0cc2d3aaa7 | ||
![]() |
67814505b3 | ||
![]() |
bae29c6d62 | ||
![]() |
a0e67d4c03 | ||
![]() |
131bc5fbf7 | ||
![]() |
051218e29b | ||
![]() |
6ace8307d8 | ||
![]() |
e84bef44b7 | ||
![]() |
3186d762f2 | ||
![]() |
c97a3b0a56 | ||
![]() |
78f1bb3b91 | ||
![]() |
67707fbc90 | ||
![]() |
2a57ffa615 | ||
![]() |
216fce74f8 | ||
![]() |
6cd3e6652a | ||
![]() |
fe7d79cee6 | ||
![]() |
2f4e7b388b | ||
![]() |
2e289cd152 | ||
![]() |
21a3dcf06c | ||
![]() |
7f56add914 | ||
![]() |
88701c6167 | ||
![]() |
e4ce6117a1 | ||
![]() |
cec2a61bdf | ||
![]() |
8275ac5853 | ||
![]() |
b7bcf97365 | ||
![]() |
fa28b480f1 | ||
![]() |
4bb95b7396 | ||
![]() |
5a9bd73e8b | ||
![]() |
4fe0276914 | ||
![]() |
5e8bda55b4 | ||
![]() |
d09c4898c1 | ||
![]() |
6ae67ed299 | ||
![]() |
32ff166a74 | ||
![]() |
8feae04281 | ||
![]() |
129f9c147b | ||
![]() |
6e336dd207 | ||
![]() |
161561c48a | ||
![]() |
c162e84383 | ||
![]() |
dc8d80a6e5 | ||
![]() |
293f67968c | ||
![]() |
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 | ||
![]() |
744efa30f2 | ||
![]() |
bf4a94dc48 | ||
![]() |
ce4ba2f6f1 | ||
![]() |
5b232b5d35 | ||
![]() |
35151bbac7 | ||
![]() |
f0e959319e | ||
![]() |
d0c4475724 | ||
![]() |
99935f1e59 | ||
![]() |
fbb43821ba | ||
![]() |
c7f5c6c1d1 | ||
![]() |
d26f1fa371 | ||
![]() |
c3718ff7dd | ||
![]() |
d63493a859 | ||
![]() |
a72183851a | ||
![]() |
40b2387667 | ||
![]() |
d814aa36a7 | ||
![]() |
e37eebe4ad | ||
![]() |
0baaaefdf8 | ||
![]() |
58a58906e7 | ||
![]() |
bec0d9b00e | ||
![]() |
e6a4ab789b | ||
![]() |
36c1d3230c | ||
![]() |
30466ec3fe | ||
![]() |
ce414a5ca9 | ||
![]() |
e4e6edd573 | ||
![]() |
79927f4dc9 | ||
![]() |
603b833757 | ||
![]() |
ba99d1a10d | ||
![]() |
efe97e8f51 | ||
![]() |
5ec23bb7ab | ||
![]() |
9b4d01ab75 | ||
![]() |
40191a88d4 | ||
![]() |
a19477d179 | ||
![]() |
bf98a78f3d | ||
![]() |
ba4c2fc1bd | ||
![]() |
b56e9ef028 | ||
![]() |
dbbd34c520 | ||
![]() |
ccb69dbdfa | ||
![]() |
11e555ef6f | ||
![]() |
61e17395c9 | ||
![]() |
733ce3b6b8 | ||
![]() |
375f143199 | ||
![]() |
2419f35eb9 | ||
![]() |
21867c3576 | ||
![]() |
28853b28bc | ||
![]() |
e2f27568a5 | ||
![]() |
98b2b796b0 | ||
![]() |
b8f3fcf00b | ||
![]() |
d3fda9a821 | ||
![]() |
19e69dc13e | ||
![]() |
48543a2dad | ||
![]() |
b22f5ae5c2 | ||
![]() |
2acb6a28fe | ||
![]() |
1064cdb79d | ||
![]() |
bd7cb1c877 | ||
![]() |
6c314982dc | ||
![]() |
d54710f113 | ||
![]() |
1346156ecd | ||
![]() |
a2d9f9b417 | ||
![]() |
3de78cca2d | ||
![]() |
5fa7cd9fa9 | ||
![]() |
a78c00fb41 | ||
![]() |
edc2a03d1c | ||
![]() |
174f8f5823 | ||
![]() |
9fbc94e8d8 | ||
![]() |
6aff35196d | ||
![]() |
eceed4ed74 | ||
![]() |
7428731eac | ||
![]() |
89b07ea0ae | ||
![]() |
d16daf0fd9 | ||
![]() |
211ab4eea8 | ||
![]() |
dbd53f8d14 |
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -18,8 +18,8 @@
|
||||
<!--
|
||||
Describe the big picture of your changes here to communicate to the
|
||||
maintainers why we should accept this pull request. If it fixes a bug
|
||||
or resolves a feature request, be sure to link to that issue in the
|
||||
additional information section.
|
||||
or resolves a feature request, be sure to link to that issue or discussion
|
||||
in the additional information section.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
@@ -56,7 +56,7 @@
|
||||
-->
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue:
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
|
||||
## Checklist
|
||||
|
27
.github/lock.yml
vendored
27
.github/lock.yml
vendored
@@ -1,27 +0,0 @@
|
||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 1
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2020-01-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
|
||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||
issues:
|
||||
daysUntilLock: 30
|
56
.github/stale.yml
vendored
56
.github/stale.yml
vendored
@@ -1,56 +0,0 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- feature request
|
||||
- Help wanted
|
||||
- to do
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
There hasn't been any activity on this issue recently. Due to the high number
|
||||
of incoming GitHub notifications, we have to clean some of the old issues,
|
||||
as many of them have already been resolved with the latest updates.
|
||||
|
||||
Please make sure to update to the latest Home Assistant version and check
|
||||
if that solves the issue. Let us know if that works for you by adding a
|
||||
comment 👍
|
||||
|
||||
This issue now has been marked as stale and will be closed if no further
|
||||
activity occurs. Thank you for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
# closeComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
20
.github/workflows/lock.yml
vendored
Normal file
20
.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Lock
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-lock-reason: ""
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-lock-reason: ""
|
42
.github/workflows/stale.yml
vendored
Normal file
42
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Stale
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v3.0.13
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
operations-per-run: 25
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request"
|
||||
stale-issue-message: >
|
||||
There hasn't been any activity on this issue recently. Due to the
|
||||
high number of incoming GitHub notifications, we have to clean some
|
||||
of the old issues, as many of them have already been resolved with
|
||||
the latest updates.
|
||||
|
||||
Please make sure to update to the latest Home Assistant version and
|
||||
check if that solves the issue. Let us know if that works for you by
|
||||
adding a comment 👍
|
||||
|
||||
This issue has now been marked as stale and will be closed if no
|
||||
further activity occurs. Thank you for your contributions.
|
||||
|
||||
stale-pr-label: "stale"
|
||||
exempt-pr-labels: "no-stale"
|
||||
stale-pr-message: >
|
||||
There hasn't been any activity on this pull request recently. This
|
||||
pull request has been automatically marked as stale because of that
|
||||
and will be closed if no further activity occurs within 7 days.
|
||||
|
||||
Thank you for your contributions.
|
@@ -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,8 +7,8 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
cards: [
|
||||
{ type: "custom:ha-demo-card" },
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
type: "grid",
|
||||
columns: 4,
|
||||
cards: [
|
||||
{
|
||||
image: "/assets/teachingbirds/isa_square.jpg",
|
||||
@@ -77,11 +77,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
],
|
||||
type: "picture-elements",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
@@ -104,8 +100,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
Mail: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
"Package and 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",
|
||||
},
|
||||
@@ -121,11 +116,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
|
||||
entity: "sensor.trash_status",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/washer_square.jpg",
|
||||
@@ -167,15 +158,10 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "input_boolean.cleaning_day",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
],
|
||||
type: "vertical-stack",
|
||||
},
|
||||
{
|
||||
type: "vertical-stack",
|
||||
cards: [
|
||||
{
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
@@ -188,11 +174,6 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "S's room",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
@@ -205,9 +186,6 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "sensor.temperature_downstairs_bathroom",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
entities: [
|
||||
|
@@ -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 () => {};
|
||||
});
|
||||
};
|
||||
|
BIN
gallery/public/images/sunflowers.jpg
Normal file
BIN
gallery/public/images/sunflowers.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
@@ -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,8 +39,12 @@ 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 id="container">
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
@@ -42,6 +54,7 @@ class DemoCards extends PolymerElement {
|
||||
></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);
|
@@ -6,7 +6,9 @@ export const createMediaPlayerEntities = () => [
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 64063,
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
entity_picture: "/images/album_cover_2.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 50,
|
||||
@@ -14,12 +16,15 @@ export const createMediaPlayerEntities = () => [
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
}),
|
||||
getEntity("media_player", "music_playing", "playing", {
|
||||
friendly_name: "Playing The Music",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
@@ -28,6 +33,7 @@ export const createMediaPlayerEntities = () => [
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
}),
|
||||
getEntity("media_player", "stream_playing", "playing", {
|
||||
friendly_name: "Playing the Stream",
|
||||
@@ -35,50 +41,125 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
supported_features: 33,
|
||||
// Pause + Next Track + Play + Browse Media
|
||||
supported_features: 147489,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
friendly_name: "Pause, No skip, tvshow",
|
||||
getEntity("media_player", "stream_paused", "paused", {
|
||||
friendly_name: "Paused the Stream",
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
// Pause + Next Track + Play
|
||||
supported_features: 16417,
|
||||
}),
|
||||
getEntity("media_player", "stream_playing_previous", "playing", {
|
||||
friendly_name: 'Playing the Stream (with "previous" support)',
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
// Pause + Previous Track + Play
|
||||
supported_features: 16401,
|
||||
}),
|
||||
getEntity("media_player", "tv_playing", "playing", {
|
||||
friendly_name: "Playing non-skip TV Show",
|
||||
media_content_type: "tvshow",
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
entity_picture: "/images/netflix.jpg",
|
||||
// Pause
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "sonos_idle", "idle", {
|
||||
friendly_name: "Sonos Idle",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
volume_level: 0.33,
|
||||
is_volume_muted: true,
|
||||
}),
|
||||
getEntity("media_player", "theater", "off", {
|
||||
getEntity("media_player", "idle_browse_media", "idle", {
|
||||
friendly_name: "Idle waiting for Browse Media (e.g. Spotify)",
|
||||
// Pause + Seek + Volume Set + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Play + Shuffle Set + Browse Media
|
||||
supported_features: 182839,
|
||||
volume_level: 0.79,
|
||||
}),
|
||||
getEntity("media_player", "theater_off", "off", {
|
||||
friendly_name: "TV Off",
|
||||
// On + Off + Play + Next + Pause
|
||||
supported_features: 16801,
|
||||
}),
|
||||
getEntity("media_player", "theater_on", "on", {
|
||||
friendly_name: "TV On",
|
||||
// On + Off + Play + Next + Pause
|
||||
supported_features: 16801,
|
||||
}),
|
||||
getEntity("media_player", "theater_off_static", "off", {
|
||||
friendly_name: "TV Off (cannot be switched on)",
|
||||
// Off + Next + Pause
|
||||
supported_features: 289,
|
||||
}),
|
||||
getEntity("media_player", "theater_on_static", "on", {
|
||||
friendly_name: "TV On (cannot be switched off)",
|
||||
// On + Next + Pause
|
||||
supported_features: 161,
|
||||
}),
|
||||
getEntity("media_player", "android_cast", "playing", {
|
||||
friendly_name: "Casting App",
|
||||
friendly_name: "Casting App (no supported features)",
|
||||
media_title: "Android Screen Casting",
|
||||
app_name: "Screen Mirroring",
|
||||
// supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "image_display", "playing", {
|
||||
friendly_name: "Digital Picture Frame",
|
||||
media_content_type: "image",
|
||||
media_title: "Famous Painting",
|
||||
media_artist: "Famous Artist",
|
||||
entity_picture: "/images/sunflowers.jpg",
|
||||
// On + Off + Browse Media
|
||||
supported_features: 131456,
|
||||
}),
|
||||
getEntity("media_player", "unavailable", "unavailable", {
|
||||
friendly_name: "Player Unavailable",
|
||||
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
|
||||
// Play Media + Stop + Play
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "unknown", "unknown", {
|
||||
friendly_name: "Player Unknown",
|
||||
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
|
||||
// Play Media + Stop + Play
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "playing", "playing", {
|
||||
friendly_name: "Player Playing (no Pause support)",
|
||||
// Volume Set + Volume Mute + Previous Track + Next Track +
|
||||
// Play Media + Stop + Play
|
||||
supported_features: 21436,
|
||||
volume_level: 1,
|
||||
}),
|
||||
getEntity("media_player", "idle", "idle", {
|
||||
friendly_name: "Player Idle",
|
||||
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
|
||||
// Play Media + Stop + Play
|
||||
supported_features: 21437,
|
||||
volume_level: 0,
|
||||
}),
|
||||
getEntity("media_player", "receiver_on", "on", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
volume_level: 0.63,
|
||||
is_volume_muted: false,
|
||||
source: "TV",
|
||||
friendly_name: "Receiver",
|
||||
friendly_name: "Receiver (selectable sources)",
|
||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||
supported_features: 84364,
|
||||
}),
|
||||
getEntity("media_player", "receiver_off", "off", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
friendly_name: "Receiver",
|
||||
friendly_name: "Receiver (selectable sources)",
|
||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||
supported_features: 84364,
|
||||
}),
|
||||
];
|
||||
|
@@ -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);
|
||||
|
@@ -7,40 +7,61 @@ import { createMediaPlayerEntities } from "../data/media_players";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Paused music",
|
||||
heading: "Paused Music",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.music_paused
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Playing music",
|
||||
heading: "Playing Music",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.music_playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Playing stream",
|
||||
heading: "Playing Stream",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.stream_playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Pause, No skip, tvshow",
|
||||
heading: "Paused Stream",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.living_room
|
||||
entity: media_player.stream_paused
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Screen casting",
|
||||
heading: 'Playing Stream (with "previous" support)',
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.stream_playing_previous
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Playing non-skip TV Show",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.tv_playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Screen Casting",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.android_cast
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Digital Picture Frame",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.image_display
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Sonos Idle",
|
||||
config: `
|
||||
@@ -48,11 +69,53 @@ const CONFIGS = [
|
||||
entity: media_player.sonos_idle
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Idle waiting for Browse Media",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.idle_browse_media
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Off",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater
|
||||
entity: media_player.theater_off
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player On",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater_on
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Off (cannot be switched on)",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater_off_static
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player On (cannot be switched off)",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater_on_static
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Idle",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.idle
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Playing",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -70,14 +133,14 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Receiver On",
|
||||
heading: "Receiver On (selectable sources)",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.receiver_on
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Receiver Off",
|
||||
heading: "Receiver Off (selectable sources)",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.receiver_off
|
||||
|
@@ -12,23 +12,45 @@ const CONFIGS = [
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: media_player.music_paused
|
||||
name: Paused music
|
||||
name: Paused Music
|
||||
- entity: media_player.music_playing
|
||||
name: Playing music
|
||||
name: Playing Music
|
||||
- entity: media_player.stream_playing
|
||||
name: Paused, no play
|
||||
- entity: media_player.living_room
|
||||
name: Pause, No skip, tvshow
|
||||
name: Playing Stream
|
||||
- entity: media_player.stream_paused
|
||||
name: Paused Stream
|
||||
- entity: media_player.stream_playing_previous
|
||||
name: Playing Stream (with "previous" support)
|
||||
- entity: media_player.tv_playing
|
||||
name: Playing non-skip TV Show
|
||||
- entity: media_player.android_cast
|
||||
name: Screen casting
|
||||
- entity: media_player.image_display
|
||||
name: Digital Picture Frame
|
||||
- entity: media_player.sonos_idle
|
||||
name: Chromcast Idle
|
||||
- entity: media_player.theater
|
||||
name: Sonos Idle
|
||||
- entity: media_player.idle_browse_media
|
||||
name: Idle waiting for Browse Media
|
||||
- entity: media_player.theater_off
|
||||
name: Player Off
|
||||
- entity: media_player.theater_on
|
||||
name: Player On
|
||||
- entity: media_player.theater_off_static
|
||||
name: Player Off (cannot be switched on)
|
||||
- entity: media_player.theater_on_static
|
||||
name: Player On (cannot be switched off)
|
||||
- entity: media_player.idle
|
||||
name: Player Idle
|
||||
- entity: media_player.playing
|
||||
name: Player Playing
|
||||
- entity: media_player.unavailable
|
||||
name: Player Unavailable
|
||||
- entity: media_player.unknown
|
||||
name: Player Unknown
|
||||
- entity: media_player.receiver_on
|
||||
name: Receiver On (selectable sources)
|
||||
- entity: media_player.receiver_off
|
||||
name: Receiver Off (selectable sources)
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
@@ -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> `;
|
||||
@@ -44,6 +58,7 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.header=${this._addon.name}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
@@ -83,10 +98,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 +131,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 +144,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;
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
],
|
||||
|
BIN
public/static/images/conference.png
Normal file
BIN
public/static/images/conference.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20201021.1",
|
||||
version="20201111.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -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),
|
||||
|
24
src/common/color/hex.ts
Normal file
24
src/common/color/hex.ts
Normal file
@@ -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}`;
|
||||
};
|
18
src/common/config/can_show_page.ts
Normal file
18
src/common/config/can_show_page.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isComponentLoaded } from "./is_component_loaded";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => {
|
||||
return (
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
!hideAdvancedPage(hass, page)
|
||||
);
|
||||
};
|
||||
|
||||
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
!page.component || isComponentLoaded(hass, page.component);
|
||||
const isCore = (page: PageNavigation) => page.core;
|
||||
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
|
||||
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
|
||||
const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
isAdvancedPage(page) && !userWantsAdvanced(hass);
|
@@ -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";
|
||||
@@ -77,3 +78,25 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
return "hass:window-open";
|
||||
}
|
||||
};
|
||||
|
||||
export const computeOpenIcon = (stateObj: HassEntity): string => {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
return "hass:arrow-expand-horizontal";
|
||||
default:
|
||||
return "hass:arrow-up";
|
||||
}
|
||||
};
|
||||
|
||||
export const computeCloseIcon = (stateObj: HassEntity): string => {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
return "hass:arrow-collapse-horizontal";
|
||||
default:
|
||||
return "hass:arrow-down";
|
||||
}
|
||||
};
|
||||
|
@@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
|
||||
export interface ScorableTextItem {
|
||||
score?: number;
|
||||
text: string;
|
||||
filterText: string;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = item.altText
|
||||
? fuzzySequentialMatch(filter, item.text, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.text);
|
||||
? fuzzySequentialMatch(filter, item.filterText, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.filterText);
|
||||
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
|
||||
);
|
||||
|
22
src/common/string/number-format.ts
Normal file
22
src/common/string/number-format.ts
Normal file
@@ -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 = {};
|
||||
|
||||
|
8
src/common/util/copy-clipboard.ts
Normal file
8
src/common/util/copy-clipboard.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const copyToClipboard = (str) => {
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
};
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "../ha-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -38,6 +38,8 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-devices-picker";
|
||||
import "../ha-svg-icon";
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
@@ -62,7 +64,7 @@ const rowRenderer = (
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
ha-icon-button {
|
||||
mwc-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
@@ -324,36 +326,34 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<div class="suffix" slot="suffix">
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
? html`<mwc-icon-button
|
||||
class="clear-button"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</ha-icon-button>
|
||||
`
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button> `
|
||||
: ""}
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</ha-icon-button>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
@@ -409,10 +409,12 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
.suffix {
|
||||
display: flex;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 0px 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
|
@@ -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;
|
||||
|
@@ -19,6 +19,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { formatAttributeName } from "../../util/hass-attributes-util";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
@@ -35,7 +36,9 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
|
||||
<paper-item></paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector("paper-item")!.textContent = model.item;
|
||||
root.querySelector("paper-item")!.textContent = formatAttributeName(
|
||||
model.item
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
@@ -92,7 +95,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
)}
|
||||
.value=${this._value}
|
||||
.value=${this._value ? formatAttributeName(this._value) : ""}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
@@ -140,7 +143,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
return this.value;
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
|
@@ -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);
|
@@ -1,124 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
/* eslint-plugin-disable lit */
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
|
||||
class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate} ${this.stateBadgeTemplate} ${this.infoTemplate}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styleTemplate() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
:host([rtl]) state-badge {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 56px;
|
||||
}
|
||||
|
||||
:host([rtl]) .info {
|
||||
margin-right: 56px;
|
||||
margin-left: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--primary-text-color);
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.name[in-dialog],
|
||||
:host([secondary-line]) .name {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.time-ago,
|
||||
.extra-info,
|
||||
.extra-info > * {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
static get stateBadgeTemplate() {
|
||||
return html` <state-badge state-obj="[[stateObj]]"></state-badge> `;
|
||||
}
|
||||
|
||||
static get infoTemplate() {
|
||||
return html`
|
||||
<div class="info">
|
||||
<div class="name" in-dialog$="[[inDialog]]">
|
||||
[[computeStateName(stateObj)]]
|
||||
</div>
|
||||
<template is="dom-if" if="[[inDialog]]">
|
||||
<div class="time-ago">
|
||||
<ha-relative-time
|
||||
id="last_changed"
|
||||
hass="[[hass]]"
|
||||
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>
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!inDialog]]">
|
||||
<div class="extra-info"><slot> </slot></div>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
inDialog: {
|
||||
type: Boolean,
|
||||
value: () => false,
|
||||
},
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeStateName(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("state-info", StateInfo);
|
158
src/components/entity/state-info.ts
Normal file
158
src/components/entity/state-info.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ type: Boolean }) public inDialog = false;
|
||||
|
||||
// property used only in css
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`<state-badge
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
<div class="info">
|
||||
<div class="name" .inDialog=${this.inDialog}>
|
||||
${computeStateName(this.stateObj)}
|
||||
</div>
|
||||
${this.inDialog
|
||||
? html`<div class="time-ago">
|
||||
<ha-relative-time
|
||||
id="last_changed"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
></ha-relative-time>
|
||||
<paper-tooltip animation-delay="0" for="last_changed">
|
||||
<div>
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_changed"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_updated"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</paper-tooltip>
|
||||
</div>`
|
||||
: html`<div class="extra-info"><slot> </slot></div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
:host([rtl]) state-badge {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 56px;
|
||||
}
|
||||
|
||||
:host([rtl]) .info {
|
||||
margin-right: 56px;
|
||||
margin-left: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--primary-text-color);
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.name[in-dialog],
|
||||
:host([secondary-line]) .name {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.time-ago,
|
||||
.extra-info,
|
||||
.extra-info > * {
|
||||
@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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"state-info": StateInfo;
|
||||
}
|
||||
}
|
@@ -9,7 +9,9 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
import hassAttributeUtil, {
|
||||
formatAttributeName,
|
||||
} from "../util/hass-attributes-util";
|
||||
|
||||
let jsYamlPromise: Promise<typeof import("js-yaml")>;
|
||||
|
||||
@@ -34,7 +36,7 @@ class HaAttributes extends LitElement {
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${attribute.replace(/_/g, " ").replace("id", "ID")}
|
||||
${formatAttributeName(attribute)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.formatAttribute(attribute)}
|
||||
@@ -61,15 +63,16 @@ class HaAttributes extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
max-width: 50%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
.key:first-letter {
|
||||
text-transform: capitalize;
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
}
|
||||
pre {
|
||||
font-family: inherit;
|
||||
|
@@ -11,7 +11,7 @@ export class HaCircularProgress extends CircularProgress {
|
||||
public alt = "Loading";
|
||||
|
||||
@property()
|
||||
public size: "small" | "medium" | "large" = "medium";
|
||||
public size: "tiny" | "small" | "medium" | "large" = "medium";
|
||||
|
||||
// @ts-ignore
|
||||
public set density(_) {
|
||||
@@ -20,6 +20,8 @@ export class HaCircularProgress extends CircularProgress {
|
||||
|
||||
public get density() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
return -8;
|
||||
case "small":
|
||||
return -5;
|
||||
case "medium":
|
||||
|
@@ -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);
|
139
src/components/ha-climate-state.ts
Normal file
139
src/components/ha-climate-state.ts
Normal file
@@ -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) {
|
||||
|
@@ -1,126 +0,0 @@
|
||||
import "./ha-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
|
||||
class HaCoverControls extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.state {
|
||||
white-space: nowrap;
|
||||
}
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="state">
|
||||
<ha-icon-button
|
||||
aria-label="Open cover"
|
||||
icon="[[computeOpenIcon(stateObj)]]"
|
||||
on-click="onOpenTap"
|
||||
invisible$="[[!entityObj.supportsOpen]]"
|
||||
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
aria-label="Stop the cover from moving"
|
||||
icon="hass:stop"
|
||||
on-click="onStopTap"
|
||||
invisible$="[[!entityObj.supportsStop]]"
|
||||
disabled="[[computeStopDisabled(stateObj)]]"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
aria-label="Close cover"
|
||||
icon="[[computeCloseIcon(stateObj)]]"
|
||||
on-click="onCloseTap"
|
||||
invisible$="[[!entityObj.supportsClose]]"
|
||||
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: "computeEntityObj(hass, stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeOpenIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
return "hass:arrow-expand-horizontal";
|
||||
default:
|
||||
return "hass:arrow-up";
|
||||
}
|
||||
}
|
||||
|
||||
computeCloseIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
return "hass:arrow-collapse-horizontal";
|
||||
default:
|
||||
return "hass:arrow-down";
|
||||
}
|
||||
}
|
||||
|
||||
computeStopDisabled(stateObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
}
|
||||
|
||||
computeClosedDisabled(stateObj, entityObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
||||
}
|
||||
|
||||
onOpenTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.openCover();
|
||||
}
|
||||
|
||||
onCloseTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.closeCover();
|
||||
}
|
||||
|
||||
onStopTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.stopCover();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-cover-controls", HaCoverControls);
|
135
src/components/ha-cover-controls.ts
Normal file
135
src/components/ha-cover-controls.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
|
||||
import "./ha-icon-button";
|
||||
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
|
||||
|
||||
@customElement("ha-cover-controls")
|
||||
class HaCoverControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@internalProperty() private _entityObj?: CoverEntity;
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="state">
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsOpen,
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_cover"
|
||||
)}
|
||||
.icon=${computeOpenIcon(this.stateObj)}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${this._computeOpenDisabled()}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsStop,
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.stop_cover"
|
||||
)}
|
||||
icon="hass:stop"
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsClose,
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.close_cover"
|
||||
)}
|
||||
.icon=${computeCloseIcon(this.stateObj)}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${this._computeClosedDisabled()}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeOpenDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
|
||||
!assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
|
||||
!assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCover();
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCover();
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCover();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.state {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hidden {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-cover-controls": HaCoverControls;
|
||||
}
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "./ha-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
|
||||
class HaCoverTiltControls extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
:host {
|
||||
white-space: nowrap;
|
||||
}
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
<ha-icon-button
|
||||
aria-label="Open cover tilt"
|
||||
icon="hass:arrow-top-right"
|
||||
on-click="onOpenTiltTap"
|
||||
title="Open tilt"
|
||||
invisible$="[[!entityObj.supportsOpenTilt]]"
|
||||
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
aria-label="Stop cover from moving"
|
||||
icon="hass:stop"
|
||||
on-click="onStopTiltTap"
|
||||
invisible$="[[!entityObj.supportsStopTilt]]"
|
||||
disabled="[[computeStopDisabled(stateObj)]]"
|
||||
title="Stop tilt"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
aria-label="Close cover tilt"
|
||||
icon="hass:arrow-bottom-left"
|
||||
on-click="onCloseTiltTap"
|
||||
title="Close tilt"
|
||||
invisible$="[[!entityObj.supportsCloseTilt]]"
|
||||
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: "computeEntityObj(hass, stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeStopDisabled(stateObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyOpenTilt && !assumedState;
|
||||
}
|
||||
|
||||
computeClosedDisabled(stateObj, entityObj) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyClosedTilt && !assumedState;
|
||||
}
|
||||
|
||||
onOpenTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.openCoverTilt();
|
||||
}
|
||||
|
||||
onCloseTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.closeCoverTilt();
|
||||
}
|
||||
|
||||
onStopTiltTap(ev) {
|
||||
ev.stopPropagation();
|
||||
this.entityObj.stopCoverTilt();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-cover-tilt-controls", HaCoverTiltControls);
|
122
src/components/ha-cover-tilt-controls.ts
Normal file
122
src/components/ha-cover-tilt-controls.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import { HomeAssistant } from "../types";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-tilt-controls")
|
||||
class HaCoverTiltControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) stateObj!: HassEntity;
|
||||
|
||||
@internalProperty() private _entityObj?: CoverEntity;
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html` <ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStop,
|
||||
})}
|
||||
label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
)}
|
||||
icon="hass:arrow-top-right"
|
||||
@click=${this._onOpenTiltTap}
|
||||
.disabled=${this._computeOpenDisabled()}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStop,
|
||||
})}
|
||||
label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
|
||||
icon="hass:stop"
|
||||
@click=${this._onStopTiltTap}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStop,
|
||||
})}
|
||||
label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
)}
|
||||
icon="hass:arrow-bottom-left"
|
||||
@click=${this._onCloseTiltTap}
|
||||
.disabled=${this._computeClosedDisabled()}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
private _computeOpenDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyOpenTilt && !assumedState;
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyClosedTilt && !assumedState;
|
||||
}
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCoverTilt();
|
||||
}
|
||||
|
||||
private _onCloseTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCoverTilt();
|
||||
}
|
||||
|
||||
private _onStopTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCoverTilt();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.invisible {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-cover-tilt-controls": HaCoverTiltControls;
|
||||
}
|
||||
}
|
@@ -1,127 +1,70 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
|
||||
|
||||
@customElement("ha-date-input")
|
||||
export class HaDateInput extends LitElement {
|
||||
@property() public year?: string;
|
||||
const VaadinDatePicker = customElements.get("vaadin-date-picker");
|
||||
|
||||
@property() public month?: string;
|
||||
export class HaDateInput extends VaadinDatePicker {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@property() public day?: string;
|
||||
this.i18n.formatDate = this._formatISODate;
|
||||
this.i18n.parseDate = this._parseISODate;
|
||||
}
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ready() {
|
||||
super.ready();
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
:host {
|
||||
display: block;
|
||||
font-family: var(--paper-font-common-base_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-common-base_-_-webkit-font-smoothing
|
||||
);
|
||||
}
|
||||
|
||||
paper-input {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
|
||||
--paper-input-container-input_-_-moz-appearance: textfield;
|
||||
--paper-input-container-shared-input-style_-_appearance: textfield;
|
||||
--paper-input-container-input-webkit-spinner_-_-webkit-appearance: none;
|
||||
--paper-input-container-input-webkit-spinner_-_margin: 0;
|
||||
--paper-input-container-input-webkit-spinner_-_display: none;
|
||||
}
|
||||
|
||||
paper-input#year {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.date-input-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 12ex;
|
||||
margin-top: -6px;
|
||||
--material-body-font-size: 16px;
|
||||
--_material-text-field-input-line-background-color: var(--primary-text-color);
|
||||
--_material-text-field-input-line-opacity: 1;
|
||||
--material-primary-color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(styleEl);
|
||||
this._inputElement.querySelector("[part='toggle-button']").style.display =
|
||||
"none";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="date-input-wrap">
|
||||
<paper-input
|
||||
id="year"
|
||||
type="number"
|
||||
.value=${this.year}
|
||||
@change=${this._formatYear}
|
||||
maxlength="4"
|
||||
max="9999"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
no-label-float
|
||||
>
|
||||
<span suffix="" slot="suffix">-</span>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
id="month"
|
||||
type="number"
|
||||
.value=${this.month}
|
||||
@change=${this._formatMonth}
|
||||
maxlength="2"
|
||||
max="12"
|
||||
min="1"
|
||||
.disabled=${this.disabled}
|
||||
no-label-float
|
||||
>
|
||||
<span suffix="" slot="suffix">-</span>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
id="day"
|
||||
type="number"
|
||||
.value=${this.day}
|
||||
@change=${this._formatDay}
|
||||
maxlength="2"
|
||||
max="31"
|
||||
min="1"
|
||||
.disabled=${this.disabled}
|
||||
no-label-float
|
||||
>
|
||||
</paper-input>
|
||||
</div>
|
||||
`;
|
||||
private _formatISODate(d) {
|
||||
return [
|
||||
("0000" + String(d.year)).slice(-4),
|
||||
("0" + String(d.month + 1)).slice(-2),
|
||||
("0" + String(d.day)).slice(-2),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
private _formatYear() {
|
||||
const yearElement = this.shadowRoot!.getElementById(
|
||||
"year"
|
||||
) as PaperInputElement;
|
||||
this.year = yearElement.value!;
|
||||
private _parseISODate(text) {
|
||||
const parts = text.split("-");
|
||||
const today = new Date();
|
||||
let date;
|
||||
let month = today.getMonth();
|
||||
let year = today.getFullYear();
|
||||
if (parts.length === 3) {
|
||||
year = parseInt(parts[0]);
|
||||
if (parts[0].length < 3 && year >= 0) {
|
||||
year += year < 50 ? 2000 : 1900;
|
||||
}
|
||||
month = parseInt(parts[1]) - 1;
|
||||
date = parseInt(parts[2]);
|
||||
} else if (parts.length === 2) {
|
||||
month = parseInt(parts[0]) - 1;
|
||||
date = parseInt(parts[1]);
|
||||
} else if (parts.length === 1) {
|
||||
date = parseInt(parts[0]);
|
||||
}
|
||||
|
||||
private _formatMonth() {
|
||||
const monthElement = this.shadowRoot!.getElementById(
|
||||
"month"
|
||||
) as PaperInputElement;
|
||||
this.month = ("0" + monthElement.value!).slice(-2);
|
||||
if (date !== undefined) {
|
||||
return { day: date, month, year };
|
||||
}
|
||||
|
||||
private _formatDay() {
|
||||
const dayElement = this.shadowRoot!.getElementById(
|
||||
"day"
|
||||
) as PaperInputElement;
|
||||
this.day = ("0" + dayElement.value!).slice(-2);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return `${this.year}-${this.month}-${this.day}`;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-date-input", HaDateInput as any);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-date-input": HaDateInput;
|
||||
|
@@ -40,6 +40,7 @@ export class HaDialog extends MwcDialog {
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
|
@@ -104,7 +104,6 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
const videoEl = this._videoEl;
|
||||
const playlist_url = this.url.replace("master_playlist", "playlist");
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
@@ -126,13 +125,30 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
this._useExoPlayer = await useExoPlayerPromise;
|
||||
let hevcRegexp: RegExp;
|
||||
let masterPlaylist: string;
|
||||
if (this._useExoPlayer) {
|
||||
hevcRegexp = /CODECS=".*?((hev1)|(hvc1))\..*?"/;
|
||||
masterPlaylist = await (await masterPlaylistPromise).text();
|
||||
const masterPlaylist = await (await masterPlaylistPromise).text();
|
||||
|
||||
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
|
||||
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
|
||||
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g;
|
||||
const match = playlistRegexp.exec(masterPlaylist);
|
||||
const matchTwice = playlistRegexp.exec(masterPlaylist);
|
||||
|
||||
// Get the regular playlist url from the input (master) playlist, falling back to the input playlist if necessary
|
||||
// This avoids the player having to load and parse the master playlist again before loading the regular playlist
|
||||
let playlist_url: string;
|
||||
if (match !== null && matchTwice === null) {
|
||||
// Only send the regular playlist url if we match exactly once
|
||||
playlist_url = new URL(match.groups!.streamUrl, this.url).href;
|
||||
} else {
|
||||
playlist_url = this.url;
|
||||
}
|
||||
if (this._useExoPlayer && hevcRegexp!.test(masterPlaylist!)) {
|
||||
|
||||
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
|
||||
if (
|
||||
this._useExoPlayer &&
|
||||
match !== null &&
|
||||
match.groups!.isHevc !== undefined
|
||||
) {
|
||||
this._renderHLSExoPlayer(playlist_url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, playlist_url);
|
||||
|
@@ -9,11 +9,16 @@ import {
|
||||
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-icon-button-arrow-next")
|
||||
export class HaIconButtonArrowNext extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _icon = mdiArrowRight;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -29,7 +34,10 @@ export class HaIconButtonArrowNext extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<mwc-icon-button .disabled=${this.disabled}>
|
||||
return html`<mwc-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
>
|
||||
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
|
||||
</mwc-icon-button> `;
|
||||
}
|
||||
|
@@ -9,11 +9,16 @@ import {
|
||||
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-icon-button-arrow-prev")
|
||||
export class HaIconButtonArrowPrev extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _icon = mdiArrowLeft;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -29,9 +34,14 @@ export class HaIconButtonArrowPrev extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<mwc-icon-button .disabled=${this.disabled}>
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
>
|
||||
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
|
||||
</mwc-icon-button> `;
|
||||
</mwc-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,11 +9,16 @@ import {
|
||||
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
|
||||
import "@material/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-icon-button-next")
|
||||
export class HaIconButtonNext extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _icon = mdiChevronRight;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -29,9 +34,14 @@ export class HaIconButtonNext extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<mwc-icon-button .disabled=${this.disabled}>
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
|
||||
>
|
||||
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
|
||||
</mwc-icon-button> `;
|
||||
</mwc-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,11 +9,16 @@ import {
|
||||
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-icon-button-prev")
|
||||
export class HaIconButtonPrev extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _icon = mdiChevronLeft;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -29,9 +34,14 @@ export class HaIconButtonPrev extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<mwc-icon-button .disabled=${this.disabled}>
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
|
||||
>
|
||||
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
|
||||
</mwc-icon-button> `;
|
||||
</mwc-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
margin: 4px 0 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
|
@@ -47,7 +47,6 @@ class HaMarkdownElement extends UpdatingElement {
|
||||
node.host !== document.location.host
|
||||
) {
|
||||
node.target = "_blank";
|
||||
node.rel = "noreferrer";
|
||||
|
||||
// protect referrer on external links and deny window.opener access for security reasons
|
||||
// (see https://mathiasbynens.github.io/rel-noopener/)
|
||||
|
@@ -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;
|
||||
|
@@ -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,213 @@ 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>
|
||||
`
|
||||
: ""}
|
||||
${this.editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
${this.hass.localize("ui.sidebar.done")}
|
||||
</mwc-button>`
|
||||
: html`<div class="title">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;
|
||||
}
|
||||
@@ -725,7 +754,7 @@ class HaSidebar extends LitElement {
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 64px;
|
||||
width: 56px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: 256px;
|
||||
@@ -737,8 +766,9 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
.menu {
|
||||
height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
padding: 0 4px;
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
@@ -747,11 +777,11 @@ class HaSidebar extends LitElement {
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||
padding-left: calc(4px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) .menu {
|
||||
padding-left: 8.5px;
|
||||
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||
padding-left: 4px;
|
||||
padding-right: calc(4px + env(safe-area-inset-right));
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
@@ -762,25 +792,26 @@ class HaSidebar extends LitElement {
|
||||
.menu mwc-icon-button {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
:host([expanded]) .menu mwc-icon-button {
|
||||
margin-right: 23px;
|
||||
}
|
||||
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||
margin-right: 0px;
|
||||
margin-left: 23px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 19px;
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([rtl]) .title {
|
||||
margin-left: 0;
|
||||
margin-right: 19px;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
:host([expanded]) .menu mwc-button {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.menu mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
#sortable,
|
||||
@@ -819,14 +850,14 @@ class HaSidebar extends LitElement {
|
||||
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px 8px;
|
||||
margin: 4px;
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
}
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
width: 248px;
|
||||
}
|
||||
:host([rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
@@ -843,9 +874,9 @@ class HaSidebar extends LitElement {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
left: 2px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
|
@@ -99,13 +99,13 @@ export class HaTab extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
height: var(--header-height);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@@ -34,8 +34,8 @@ export class HaTabs extends PaperTabs {
|
||||
|
||||
superStyle!.appendChild(
|
||||
document.createTextNode(`
|
||||
:host {
|
||||
padding-top: .5px;
|
||||
#selectionBar {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.not-visible {
|
||||
display: none;
|
||||
|
@@ -540,7 +540,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
this._loading = true;
|
||||
const itemData =
|
||||
let itemData: any;
|
||||
try {
|
||||
itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
@@ -549,8 +551,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
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"];
|
||||
|
@@ -58,8 +58,18 @@ export interface DataEntryFlowStepAbort {
|
||||
description_placeholders: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepProgress {
|
||||
type: "progress";
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
step_id: string;
|
||||
progress_action: string;
|
||||
description_placeholders: { [key: string]: string };
|
||||
}
|
||||
|
||||
export type DataEntryFlowStep =
|
||||
| DataEntryFlowStepForm
|
||||
| DataEntryFlowStepExternal
|
||||
| DataEntryFlowStepCreateEntry
|
||||
| DataEntryFlowStepAbort;
|
||||
| DataEntryFlowStepAbort
|
||||
| DataEntryFlowStepProgress;
|
||||
|
@@ -20,6 +20,12 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
original_icon?: string;
|
||||
}
|
||||
|
||||
export interface UpdateEntityRegistryEntryResult {
|
||||
entity_entry: ExtEntityRegistryEntry;
|
||||
reload_delay?: number;
|
||||
require_restart?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityRegistryEntryUpdateParams {
|
||||
name?: string | null;
|
||||
icon?: string | null;
|
||||
@@ -72,7 +78,7 @@ export const updateEntityRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
): Promise<ExtEntityRegistryEntry> =>
|
||||
): Promise<UpdateEntityRegistryEntryResult> =>
|
||||
hass.callWS({
|
||||
type: "config/entity_registry/update",
|
||||
entity_id: entityId,
|
||||
|
26
src/data/hassio/ingress.ts
Normal file
26
src/data/hassio/ingress.ts
Normal file
@@ -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[];
|
||||
|
@@ -18,6 +18,8 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
export const SUPPORT_PAUSE = 1;
|
||||
export const SUPPORT_SEEK = 2;
|
||||
@@ -31,7 +33,7 @@ export const SUPPORT_PLAY_MEDIA = 512;
|
||||
export const SUPPORT_VOLUME_BUTTONS = 1024;
|
||||
export const SUPPORT_SELECT_SOURCE = 2048;
|
||||
export const SUPPORT_STOP = 4096;
|
||||
export const SUPPORTS_PLAY = 16384;
|
||||
export const SUPPORT_PLAY = 16384;
|
||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
export const SUPPORT_BROWSE_MEDIA = 131072;
|
||||
export const CONTRAST_RATIO = 4.5;
|
||||
@@ -166,6 +168,7 @@ export const computeMediaDescription = (stateObj: HassEntity): string => {
|
||||
|
||||
switch (stateObj.attributes.media_content_type) {
|
||||
case "music":
|
||||
case "image":
|
||||
secondaryTitle = stateObj.attributes.media_artist;
|
||||
break;
|
||||
case "playlist":
|
||||
@@ -187,3 +190,85 @@ export const computeMediaDescription = (stateObj: HassEntity): string => {
|
||||
|
||||
return secondaryTitle;
|
||||
};
|
||||
|
||||
export const computeMediaControls = (
|
||||
stateObj: HassEntity
|
||||
): ControlButton[] | undefined => {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = stateObj.state;
|
||||
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state === "off") {
|
||||
return supportsFeature(stateObj, SUPPORT_TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: "hass:power",
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const buttons: ControlButton[] = [];
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: "hass:power",
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" || state === "paused") &&
|
||||
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
|
||||
) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-previous",
|
||||
action: "media_previous_track",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
|
||||
supportsFeature(stateObj, SUPPORT_STOP))) ||
|
||||
((state === "paused" || state === "idle") &&
|
||||
supportsFeature(stateObj, SUPPORT_PLAY)) ||
|
||||
(state === "on" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PLAY) ||
|
||||
supportsFeature(stateObj, SUPPORT_PAUSE)))
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
state === "on"
|
||||
? "hass:play-pause"
|
||||
: state !== "playing"
|
||||
? "hass:play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action:
|
||||
state === "playing" && !supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_stop"
|
||||
: "media_play_pause",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" || state === "paused") &&
|
||||
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
|
||||
) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-next",
|
||||
action: "media_next_track",
|
||||
});
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
};
|
||||
|
@@ -21,6 +21,18 @@ export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||
? hass.panels[hass.defaultPanel]
|
||||
: hass.panels[DEFAULT_PANEL];
|
||||
|
||||
export const getPanelNameTranslationKey = (panel: PanelInfo): string => {
|
||||
if (panel.url_path === "lovelace") {
|
||||
return "panel.states";
|
||||
}
|
||||
|
||||
if (panel.url_path === "profile") {
|
||||
return "panel.profile";
|
||||
}
|
||||
|
||||
return `panel.${panel.title}`;
|
||||
};
|
||||
|
||||
export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
if (!hass.panels) {
|
||||
return undefined;
|
||||
@@ -34,13 +46,20 @@ export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (panel.url_path === "lovelace") {
|
||||
return hass.localize("panel.states");
|
||||
}
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
if (panel.url_path === "profile") {
|
||||
return hass.localize("panel.profile");
|
||||
}
|
||||
|
||||
return hass.localize(`panel.${panel.title}`) || panel.title || undefined;
|
||||
return hass.localize(translationKey) || panel.title || undefined;
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
case "profile":
|
||||
return "hass:account";
|
||||
case "lovelace":
|
||||
return "hass:view-dashboard";
|
||||
}
|
||||
}
|
||||
|
||||
return panel.icon;
|
||||
};
|
||||
|
@@ -1,24 +1,111 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface HomeAssistantSystemHealthInfo {
|
||||
version: string;
|
||||
dev: boolean;
|
||||
hassio: boolean;
|
||||
virtualenv: string;
|
||||
python_version: string;
|
||||
docker: boolean;
|
||||
arch: string;
|
||||
timezone: string;
|
||||
os_name: string;
|
||||
interface SystemCheckValueDateObject {
|
||||
type: "date";
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SystemCheckValueErrorObject {
|
||||
type: "failed";
|
||||
error: string;
|
||||
more_info?: string;
|
||||
}
|
||||
|
||||
interface SystemCheckValuePendingObject {
|
||||
type: "pending";
|
||||
}
|
||||
|
||||
export type SystemCheckValueObject =
|
||||
| SystemCheckValueDateObject
|
||||
| SystemCheckValueErrorObject
|
||||
| SystemCheckValuePendingObject;
|
||||
|
||||
export type SystemCheckValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| SystemCheckValueObject;
|
||||
|
||||
export interface SystemHealthInfo {
|
||||
[domain: string]: { [key: string]: string | number | boolean };
|
||||
[domain: string]: {
|
||||
manage_url?: string;
|
||||
info: {
|
||||
[key: string]: SystemCheckValue;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchSystemHealthInfo = (
|
||||
hass: HomeAssistant
|
||||
): Promise<SystemHealthInfo> =>
|
||||
hass.callWS({
|
||||
interface SystemHealthEventInitial {
|
||||
type: "initial";
|
||||
data: SystemHealthInfo;
|
||||
}
|
||||
interface SystemHealthEventUpdateSuccess {
|
||||
type: "update";
|
||||
success: true;
|
||||
domain: string;
|
||||
key: string;
|
||||
data: SystemCheckValue;
|
||||
}
|
||||
|
||||
interface SystemHealthEventUpdateError {
|
||||
type: "update";
|
||||
success: false;
|
||||
domain: string;
|
||||
key: string;
|
||||
error: {
|
||||
msg: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SystemHealthEventFinish {
|
||||
type: "finish";
|
||||
}
|
||||
|
||||
type SystemHealthEvent =
|
||||
| SystemHealthEventInitial
|
||||
| SystemHealthEventUpdateSuccess
|
||||
| SystemHealthEventUpdateError
|
||||
| SystemHealthEventFinish;
|
||||
|
||||
export const subscribeSystemHealthInfo = (
|
||||
hass: HomeAssistant,
|
||||
callback: (info: SystemHealthInfo) => void
|
||||
) => {
|
||||
let data = {};
|
||||
|
||||
const unsubProm = hass.connection.subscribeMessage<SystemHealthEvent>(
|
||||
(updateEvent) => {
|
||||
if (updateEvent.type === "initial") {
|
||||
data = updateEvent.data;
|
||||
callback(data);
|
||||
return;
|
||||
}
|
||||
if (updateEvent.type === "finish") {
|
||||
unsubProm.then((unsub) => unsub());
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
...data,
|
||||
[updateEvent.domain]: {
|
||||
...data[updateEvent.domain],
|
||||
info: {
|
||||
...data[updateEvent.domain].info,
|
||||
[updateEvent.key]: updateEvent.success
|
||||
? updateEvent.data
|
||||
: {
|
||||
error: true,
|
||||
value: updateEvent.error.msg,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
callback(data);
|
||||
},
|
||||
{
|
||||
type: "system_health/info",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return unsubProm;
|
||||
};
|
||||
|
10
src/data/tasmota.ts
Normal file
10
src/data/tasmota.ts
Normal file
@@ -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,
|
||||
});
|
@@ -17,7 +17,8 @@ export type TranslationCategory =
|
||||
| "config"
|
||||
| "options"
|
||||
| "device_automation"
|
||||
| "mfa_setup";
|
||||
| "mfa_setup"
|
||||
| "system_health";
|
||||
|
||||
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, "language");
|
||||
|
@@ -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}"
|
||||
>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
<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
|
||||
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">
|
||||
</div>
|
||||
<mwc-button
|
||||
@click="${this._updateEntry}"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
)}
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-paper-dialog>
|
||||
<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;
|
||||
|
@@ -36,6 +36,7 @@ import "./step-flow-external";
|
||||
import "./step-flow-form";
|
||||
import "./step-flow-loading";
|
||||
import "./step-flow-pick-handler";
|
||||
import "./step-flow-progress";
|
||||
|
||||
let instance = 0;
|
||||
|
||||
@@ -195,6 +196,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
></step-flow-abort>
|
||||
`
|
||||
: this._step.type === "progress"
|
||||
? html`
|
||||
<step-flow-progress
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-progress>
|
||||
`
|
||||
: this._devices === undefined || this._areas === undefined
|
||||
? // When it's a create entry result, we will fetch device & area registry
|
||||
html` <step-flow-loading></step-flow-loading> `
|
||||
|
@@ -160,4 +160,21 @@ export const showConfigFlowDialog = (
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(hass, step) {
|
||||
return hass.localize(`component.${step.handler}.title`);
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(hass, step) {
|
||||
const description = localizeKey(
|
||||
hass.localize,
|
||||
`component.${step.handler}.config.progress.${step.progress_action}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
DataEntryFlowStepCreateEntry,
|
||||
DataEntryFlowStepExternal,
|
||||
DataEntryFlowStepForm,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@@ -68,6 +69,16 @@ export interface FlowConfig {
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepCreateEntry
|
||||
): TemplateResult | "";
|
||||
|
||||
renderShowFormProgressHeader(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepProgress
|
||||
): string;
|
||||
|
||||
renderShowFormProgressDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepProgress
|
||||
): TemplateResult | "";
|
||||
}
|
||||
|
||||
export interface DataEntryFlowDialogParams {
|
||||
|
@@ -110,5 +110,13 @@ export const showOptionsFlowDialog = (
|
||||
<p>${hass.localize(`ui.dialogs.options_flow.success.description`)}</p>
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(_hass, _step) {
|
||||
return "";
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(_hass, _step) {
|
||||
return "";
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@@ -23,6 +23,7 @@ import { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
|
||||
interface HandlerObj {
|
||||
name: string;
|
||||
@@ -102,7 +103,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
<img
|
||||
slot="item-icon"
|
||||
loading="lazy"
|
||||
src="https://brands.home-assistant.io/_/${handler.slug}/icon.png"
|
||||
src=${brandsUrl(handler.slug, "icon", true)}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
|
||||
|
82
src/dialogs/config-flow/step-flow-progress.ts
Normal file
82
src/dialogs/config-flow/step-flow-progress.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import {
|
||||
DataEntryFlowProgressedEvent,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
@customElement("step-flow-progress")
|
||||
class StepFlowProgress extends LitElement {
|
||||
public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
private step!: DataEntryFlowStepProgress;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>
|
||||
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
|
||||
</h2>
|
||||
<div class="content">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
${this.flowConfig.renderShowFormProgressDescription(
|
||||
this.hass,
|
||||
this.step
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.connection.subscribeEvents<DataEntryFlowProgressedEvent>(
|
||||
async (ev) => {
|
||||
if (ev.data.flow_id !== this.step.flow_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.fetchFlow(this.hass, this.step.flow_id),
|
||||
});
|
||||
},
|
||||
"data_entry_flow_progressed"
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.content {
|
||||
padding: 50px 100px;
|
||||
text-align: center;
|
||||
}
|
||||
ha-circular-progress {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"step-flow-progress": StepFlowProgress;
|
||||
}
|
||||
}
|
@@ -1,26 +1,28 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-area-picker";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../components/dialog/ha-paper-dialog";
|
||||
import "../../components/ha-area-picker";
|
||||
import { computeDeviceName } from "../../data/device_registry";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { computeDeviceName } from "../../data/device_registry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
|
||||
@customElement("dialog-device-registry-detail")
|
||||
class DialogDeviceRegistryDetail extends LitElement {
|
||||
@@ -34,7 +36,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
|
||||
@internalProperty() private _areaId?: string;
|
||||
|
||||
private _submitting?: boolean;
|
||||
@internalProperty() private _submitting?: boolean;
|
||||
|
||||
public async showDialog(
|
||||
params: DeviceRegistryDetailDialogParams
|
||||
@@ -46,22 +48,24 @@ class DialogDeviceRegistryDetail 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 device = this._params.device;
|
||||
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${computeDeviceName(device, this.hass)}
|
||||
>
|
||||
<h2>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
@@ -77,13 +81,22 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button @click="${this._updateEntry}">
|
||||
</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.hass.localize("ui.panel.config.devices.update")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -113,19 +126,10 @@ class DialogDeviceRegistryDetail 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 {
|
||||
min-width: 400px;
|
||||
}
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
|
||||
import "../../../components/ha-date-input";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/paper-time-input";
|
||||
@@ -14,12 +14,12 @@ class DatetimeInput extends PolymerElement {
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp="">
|
||||
<div>
|
||||
<vaadin-date-picker
|
||||
<ha-date-input
|
||||
id="dateInput"
|
||||
on-value-changed="dateTimeChanged"
|
||||
label="Date"
|
||||
value="{{selectedDate}}"
|
||||
></vaadin-date-picker>
|
||||
></ha-date-input>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp="">
|
||||
|
@@ -25,19 +25,12 @@ import "../../../components/ha-svg-icon";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
ControlButton,
|
||||
computeMediaControls,
|
||||
MediaPickedEvent,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
@@ -57,8 +50,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const controls = this._getControls();
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj);
|
||||
|
||||
return html`
|
||||
${!controls
|
||||
@@ -254,84 +247,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getControls(): ControlButton[] | undefined {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = stateObj.state;
|
||||
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state === "off") {
|
||||
return supportsFeature(stateObj, SUPPORT_TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: "hass:power",
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (state === "idle") {
|
||||
return supportsFeature(stateObj, SUPPORTS_PLAY)
|
||||
? [
|
||||
{
|
||||
icon: "hass:play",
|
||||
action: "media_play",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const buttons: ControlButton[] = [];
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: "hass:power",
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-previous",
|
||||
action: "media_previous_track",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
|
||||
supportsFeature(stateObj, SUPPORT_STOP))) ||
|
||||
(state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY))
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
state !== "playing"
|
||||
? "hass:play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action: "media_play_pause",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-next",
|
||||
action: "media_next_track",
|
||||
});
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
|
@@ -72,6 +72,12 @@ class MoreInfoSun extends LitElement {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: inline-block;
|
||||
}
|
||||
ha-relative-time::first-letter {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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}
|
||||
@@ -208,17 +193,18 @@ export class MoreInfoDialog extends LitElement {
|
||||
: html`<ha-more-info-history
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history>
|
||||
<ha-more-info-logbook
|
||||
></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>`}
|
||||
${this._moreInfoType
|
||||
? dynamicElement(this._moreInfoType, {
|
||||
hass: this.hass,
|
||||
stateObj,
|
||||
})
|
||||
: ""}
|
||||
<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, {
|
||||
@@ -279,8 +285,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.confirm_remove_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => {
|
||||
removeEntityRegistryEntry(this.hass, entityId);
|
||||
},
|
||||
|
@@ -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``;
|
||||
}
|
||||
|
||||
|
57
src/dialogs/more-info/more-info-content.ts
Normal file
57
src/dialogs/more-info/more-info-content.ts
Normal file
@@ -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);
|
@@ -25,8 +25,8 @@ export class HuiNotificationDrawer extends EventsMixin(
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
min-height: 64px;
|
||||
width: calc(100% - 32px);
|
||||
height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div[main-title] {
|
||||
@@ -63,7 +63,10 @@ export class HuiNotificationDrawer extends EventsMixin(
|
||||
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<ha-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></ha-icon-button-prev>
|
||||
<ha-icon-button-prev hass="[[hass]]" on-click="_closeDrawer"
|
||||
title="[[localize('ui.notification_drawer.close')]]"
|
||||
label="[[localize('ui.notification_drawer.close')]]">
|
||||
</ha-icon-button-prev>
|
||||
</app-toolbar>
|
||||
<div class="notifications">
|
||||
<template is="dom-if" if="[[!_empty(notifications)]]">
|
||||
|
@@ -36,30 +36,66 @@ 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";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { configSections } from "../../panels/config/ha-panel-config";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { canShowPage } from "../../common/config/can_show_page";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
|
||||
const DEFAULT_SERVER_ICON = "hass:server";
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
icon: string;
|
||||
primaryText: string;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
action(data?: any): void;
|
||||
}
|
||||
|
||||
export interface CommandItem extends QuickBarItem {
|
||||
categoryKey: "reload" | "navigation" | "server_control";
|
||||
categoryText: string;
|
||||
}
|
||||
|
||||
const isCommandItem = (
|
||||
item: QuickBarItem | CommandItem
|
||||
): item is CommandItem => {
|
||||
return (item as CommandItem).categoryKey !== undefined;
|
||||
};
|
||||
|
||||
interface QuickBarNavigationItem extends CommandItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
|
||||
|
||||
type BaseNavigationCommand = Pick<
|
||||
QuickBarNavigationItem,
|
||||
"primaryText" | "path"
|
||||
>;
|
||||
@customElement("ha-quick-bar")
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _commandItems: QuickBarItem[] = [];
|
||||
@internalProperty() private _commandItems?: CommandItem[];
|
||||
|
||||
@internalProperty() private _entityItems: QuickBarItem[] = [];
|
||||
@internalProperty() private _entityItems?: QuickBarItem[];
|
||||
|
||||
@internalProperty() private _items?: QuickBarItem[] = [];
|
||||
|
||||
@internalProperty() private _filter = "";
|
||||
|
||||
@internalProperty() private _search = "";
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _commandMode = false;
|
||||
|
||||
@internalProperty() private _commandTriggered = -1;
|
||||
@internalProperty() private _commandType?: CommandItem["categoryKey"];
|
||||
|
||||
@internalProperty() private _done = false;
|
||||
|
||||
@@ -69,8 +105,11 @@ export class QuickBar extends LitElement {
|
||||
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
||||
this._commandItems = this._generateCommandItems();
|
||||
this._entityItems = this._generateEntityItems();
|
||||
this._commandType = params.commandType;
|
||||
this._search = this._commandType
|
||||
? `${this._getCommandCategoryLabel(this._commandType).toLowerCase()} `
|
||||
: "";
|
||||
this._initializeItemsIfNeeded();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
@@ -79,7 +118,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 +155,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 +173,6 @@ export class QuickBar extends LitElement {
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`<mwc-list
|
||||
activatable
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
@@ -155,6 +193,14 @@ export class QuickBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _initializeItemsIfNeeded() {
|
||||
if (this._commandMode) {
|
||||
this._commandItems = this._commandItems || this._generateCommandItems();
|
||||
} else {
|
||||
this._entityItems = this._entityItems || this._generateEntityItems();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._setFilteredItems();
|
||||
this.updateComplete.then(() => {
|
||||
@@ -174,34 +220,62 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _renderItem(item: QuickBarItem, index?: number) {
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
: this._renderEntityItem(item, index);
|
||||
}
|
||||
|
||||
private _renderEntityItem(item: QuickBarItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
hasMeta
|
||||
graphic=${item.altText ? "avatar" : "icon"}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
|
||||
<span>${item.text}</span>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
|
||||
<span>${item.primaryText}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="secondary">${item.altText}</span>
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
`
|
||||
: null}
|
||||
${this._commandTriggered === index
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
slot="meta"
|
||||
></ha-circular-progress>`
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
>
|
||||
<span class="command-category ${item.categoryKey}"
|
||||
>${item.categoryText}
|
||||
</span>
|
||||
|
||||
<span>${item.primaryText}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||
this._commandTriggered = index;
|
||||
this._addSpinnerToCommandItem(index);
|
||||
|
||||
await item.action();
|
||||
this.closeDialog();
|
||||
@@ -231,21 +305,33 @@ 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;
|
||||
|
||||
this._initializeItemsIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,37 +359,171 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _generateCommandItems(): QuickBarItem[] {
|
||||
return [...this._generateReloadCommands()].sort((a, b) =>
|
||||
compare(a.text.toLowerCase(), b.text.toLowerCase())
|
||||
private _generateEntityItems(): QuickBarItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => {
|
||||
const primaryText = computeStateName(this.hass.states[entityId]);
|
||||
return {
|
||||
primaryText,
|
||||
filterText: primaryText,
|
||||
altText: entityId,
|
||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
};
|
||||
})
|
||||
.sort((a, b) =>
|
||||
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
private _generateReloadCommands(): QuickBarItem[] {
|
||||
private _generateCommandItems(): CommandItem[] {
|
||||
return [
|
||||
...this._generateReloadCommands(),
|
||||
...this._generateServerControlCommands(),
|
||||
...this._generateNavigationCommands(),
|
||||
].sort((a, b) =>
|
||||
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
private _generateReloadCommands(): CommandItem[] {
|
||||
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
|
||||
|
||||
return reloadableDomains.map((domain) => ({
|
||||
text:
|
||||
return reloadableDomains.map((domain) => {
|
||||
const categoryKey = "reload";
|
||||
const categoryText = this._getCommandCategoryLabel(categoryKey);
|
||||
const primaryText =
|
||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
icon: domainIcon(domain),
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
}));
|
||||
categoryKey,
|
||||
categoryText,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _generateEntityItems(): QuickBarItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => ({
|
||||
text: computeStateName(this.hass.states[entityId]),
|
||||
altText: entityId,
|
||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
}))
|
||||
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
|
||||
private _generateServerControlCommands(): CommandItem[] {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey = "server_control";
|
||||
const categoryText = this._getCommandCategoryLabel(categoryKey);
|
||||
const primaryText = this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
);
|
||||
|
||||
return this._generateConfirmationCommand(
|
||||
{
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
categoryKey,
|
||||
categoryText,
|
||||
icon: DEFAULT_SERVER_ICON,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _generateNavigationCommands(): CommandItem[] {
|
||||
const panelItems = this._generateNavigationPanelCommands();
|
||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
||||
|
||||
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
|
||||
}
|
||||
|
||||
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
|
||||
return Object.keys(this.hass.panels).map((panelKey) => {
|
||||
const panel = this.hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
const primaryText =
|
||||
this.hass.localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (canShowPage(this.hass, page)) {
|
||||
if (page.component) {
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
if (info) {
|
||||
items.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private _getNavigationInfoFromConfig(
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined {
|
||||
if (page.component) {
|
||||
const caption = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
return { ...page, primaryText: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _generateConfirmationCommand(
|
||||
item: CommandItem,
|
||||
confirmText: ConfirmationDialogParams["confirmText"]
|
||||
): CommandItem {
|
||||
return {
|
||||
...item,
|
||||
action: () =>
|
||||
showConfirmationDialog(this, {
|
||||
confirmText,
|
||||
confirm: item.action,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private _finalizeNavigationCommands(
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
const categoryKey = "navigation";
|
||||
const categoryText = this._getCommandCategoryLabel(categoryKey);
|
||||
|
||||
return {
|
||||
...item,
|
||||
categoryKey,
|
||||
categoryText,
|
||||
filterText: `${categoryText} ${item.primaryText}`,
|
||||
action: () => navigate(this, item.path),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleIfAlreadyOpened() {
|
||||
@@ -322,6 +542,10 @@ export class QuickBar extends LitElement {
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
private _getCommandCategoryLabel = (
|
||||
categoryKey: CommandItem["categoryKey"]
|
||||
) => this.hass.localize(`ui.dialogs.quick-bar.commands.types.${categoryKey}`);
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleDialog,
|
||||
@@ -345,8 +569,35 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
ha-svg-icon.prefix {
|
||||
margin: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
span.command-category {
|
||||
font-weight: bold;
|
||||
padding: 3px;
|
||||
display: inline-flex;
|
||||
border-radius: 6px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
span.command-category.reload {
|
||||
background: pink;
|
||||
}
|
||||
|
||||
span.command-category.navigation {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
span.command-category.server_control {
|
||||
background: orange;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.uni-virtualizer-host {
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CommandItem } from "./ha-quick-bar";
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
commandMode?: boolean;
|
||||
commandType?: CommandItem["categoryKey"];
|
||||
}
|
||||
|
||||
export const loadQuickBar = () =>
|
||||
|
@@ -53,9 +53,12 @@ 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 translation = await getTranslation(fragment, lang);
|
||||
const resources = {
|
||||
[lang]: {
|
||||
...(hass().resources && hass().resources[lang]),
|
||||
@@ -64,8 +67,7 @@ export const provideHass = (
|
||||
};
|
||||
hass().updateHass({
|
||||
resources,
|
||||
localize: computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
localize: await computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,8 @@
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
--disabled-text-color: #6f6f6f;
|
||||
--mdc-theme-surface: #1e1e1e;
|
||||
--ha-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
|
@@ -9,9 +9,12 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "./hass-subpage";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("hass-error-screen")
|
||||
class HassErrorScreen extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public toolbar = true;
|
||||
|
||||
@property() public error?: string;
|
||||
@@ -21,6 +24,7 @@ class HassErrorScreen extends LitElement {
|
||||
${this.toolbar
|
||||
? html`<div class="toolbar">
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._handleBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</div>`
|
||||
@@ -50,7 +54,7 @@ class HassErrorScreen extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: 65px;
|
||||
height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
|
@@ -39,6 +39,7 @@ class HassLoadingScreen extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._handleBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
@@ -66,7 +67,7 @@ class HassLoadingScreen extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: 65px;
|
||||
height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user