mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 19:29:27 +00:00
Compare commits
217 Commits
fix-long-t
...
20200904.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
61dbae8b8b | ||
![]() |
fcc22ba560 | ||
![]() |
2adeb88fe6 | ||
![]() |
e63a78bcdb | ||
![]() |
b065f002a4 | ||
![]() |
349a5f52b1 | ||
![]() |
aa5e20df05 | ||
![]() |
793b9f238c | ||
![]() |
9c4fdaa4f3 | ||
![]() |
d1a9cb488a | ||
![]() |
faee2c3e1b | ||
![]() |
b7845c318e | ||
![]() |
426a0727c3 | ||
![]() |
584e509a9c | ||
![]() |
f3639c2663 | ||
![]() |
1431e75f8b | ||
![]() |
be8812e0af | ||
![]() |
fd6436d490 | ||
![]() |
fd1342f9d1 | ||
![]() |
5fa0012195 | ||
![]() |
9dbb67ef01 | ||
![]() |
d16e2f37d4 | ||
![]() |
d9e8b53ffe | ||
![]() |
1997e63b7c | ||
![]() |
6f673359ff | ||
![]() |
45dfbff10a | ||
![]() |
348ee96274 | ||
![]() |
8edee32e77 | ||
![]() |
6d8d263ca6 | ||
![]() |
35923709e2 | ||
![]() |
fdd4d53448 | ||
![]() |
06419f662e | ||
![]() |
57763ef032 | ||
![]() |
8e506f7749 | ||
![]() |
c7f8fe1468 | ||
![]() |
4156a4e36d | ||
![]() |
ba3cc7df0f | ||
![]() |
0c212d39eb | ||
![]() |
3bd2e8dbf5 | ||
![]() |
5292119e6e | ||
![]() |
994a397231 | ||
![]() |
353b71f803 | ||
![]() |
eb12afe8cc | ||
![]() |
4a176f1b43 | ||
![]() |
8e228baa82 | ||
![]() |
154b53b0d8 | ||
![]() |
a3f680d80c | ||
![]() |
0d75fe6b81 | ||
![]() |
4070380ded | ||
![]() |
41195dcef0 | ||
![]() |
78a1e45be2 | ||
![]() |
d8e88bc58d | ||
![]() |
448e9b71b8 | ||
![]() |
2e178164cc | ||
![]() |
9f2e3f05a1 | ||
![]() |
405bd29ebd | ||
![]() |
b39b54e0ac | ||
![]() |
119c5c9071 | ||
![]() |
7a4c9b128c | ||
![]() |
dc5b92030f | ||
![]() |
db0a010d7c | ||
![]() |
a117a19bdf | ||
![]() |
5f46fdb406 | ||
![]() |
f0201de4cc | ||
![]() |
6cd51a318b | ||
![]() |
c1a4b27bc7 | ||
![]() |
5113222050 | ||
![]() |
90f12eea5e | ||
![]() |
2403743701 | ||
![]() |
3e6a759309 | ||
![]() |
35a430e9f4 | ||
![]() |
b644f7d23d | ||
![]() |
7702a05464 | ||
![]() |
493af5fe82 | ||
![]() |
ac66a59cec | ||
![]() |
e10c8faa47 | ||
![]() |
9b7d17433c | ||
![]() |
a40eb1ff43 | ||
![]() |
04df6c3e9e | ||
![]() |
1b970e5a66 | ||
![]() |
75406c2d01 | ||
![]() |
64d3511fbc | ||
![]() |
c610f54977 | ||
![]() |
090ad34f78 | ||
![]() |
358c5205d2 | ||
![]() |
5503cd0589 | ||
![]() |
dae42b1bd9 | ||
![]() |
06a25284e8 | ||
![]() |
5989560f15 | ||
![]() |
63c995e5da | ||
![]() |
dc5607f554 | ||
![]() |
d49302c032 | ||
![]() |
63fef9bd4b | ||
![]() |
6599351d45 | ||
![]() |
47e9531972 | ||
![]() |
3ba31483f4 | ||
![]() |
f4ca94f2e1 | ||
![]() |
67f9be2b77 | ||
![]() |
e2fd155e1b | ||
![]() |
931068dede | ||
![]() |
bc4c9cc40d | ||
![]() |
294665fbe8 | ||
![]() |
e8f6a79c8f | ||
![]() |
5fd8b5c5b9 | ||
![]() |
226b2a73af | ||
![]() |
42d421a6fc | ||
![]() |
a90203f256 | ||
![]() |
c3ef79caa9 | ||
![]() |
1439afcd9c | ||
![]() |
d263b19910 | ||
![]() |
1e477226ea | ||
![]() |
026fc1d2e3 | ||
![]() |
2d4bd9857a | ||
![]() |
8f48f5b45c | ||
![]() |
22210b7400 | ||
![]() |
7d05855ee0 | ||
![]() |
b2460cbc3d | ||
![]() |
4561957e56 | ||
![]() |
3367fadc3a | ||
![]() |
d7e409b042 | ||
![]() |
a0b28e8ad1 | ||
![]() |
f928a8e58e | ||
![]() |
0bc4b3d0fa | ||
![]() |
e352768388 | ||
![]() |
6835b73e49 | ||
![]() |
f1503f871b | ||
![]() |
c4d8aba5c8 | ||
![]() |
39f24c41ad | ||
![]() |
21644ec889 | ||
![]() |
613470b44d | ||
![]() |
6c918e346b | ||
![]() |
bce8539572 | ||
![]() |
aab86e00ec | ||
![]() |
2a58726caf | ||
![]() |
4163b35b32 | ||
![]() |
9c6dac8180 | ||
![]() |
80fc37724b | ||
![]() |
77b25f5132 | ||
![]() |
684f098450 | ||
![]() |
d09f74d30f | ||
![]() |
3d973b112e | ||
![]() |
96986164a4 | ||
![]() |
78152c20a9 | ||
![]() |
2bb64e9e2f | ||
![]() |
746844dfc8 | ||
![]() |
41b613a2d7 | ||
![]() |
3b3aeea224 | ||
![]() |
71c592a0ce | ||
![]() |
15193fcf5f | ||
![]() |
a31f53395f | ||
![]() |
283b134d84 | ||
![]() |
271eb614cd | ||
![]() |
16167bef07 | ||
![]() |
1eac9fa1cd | ||
![]() |
7f819f0020 | ||
![]() |
dec1f99a5f | ||
![]() |
c705e74fc8 | ||
![]() |
01df10f93e | ||
![]() |
9877f08cf4 | ||
![]() |
3dc4b1d775 | ||
![]() |
02791c51ae | ||
![]() |
49683326e6 | ||
![]() |
947773a82e | ||
![]() |
2a229df624 | ||
![]() |
e605ad5e46 | ||
![]() |
0d4f43472b | ||
![]() |
b30e467685 | ||
![]() |
a56c0b52d5 | ||
![]() |
c17ebfd279 | ||
![]() |
5400b1da96 | ||
![]() |
69f4a618b2 | ||
![]() |
16b8b6698c | ||
![]() |
b29a700d40 | ||
![]() |
bbb1468439 | ||
![]() |
72f9d6a8d3 | ||
![]() |
3ec8da1f17 | ||
![]() |
dbea3848df | ||
![]() |
33871435e1 | ||
![]() |
f1f22b43dc | ||
![]() |
2fb9a56e0b | ||
![]() |
14e8f66ed7 | ||
![]() |
e6f5072462 | ||
![]() |
a64f50fa72 | ||
![]() |
bb5f6e88d0 | ||
![]() |
6991403203 | ||
![]() |
410bd22f8a | ||
![]() |
b81d823602 | ||
![]() |
bd5115f9aa | ||
![]() |
7bcbed80d7 | ||
![]() |
8fb62ebf5f | ||
![]() |
209dd9923f | ||
![]() |
c75207e391 | ||
![]() |
d957f36927 | ||
![]() |
9ac459b6d9 | ||
![]() |
e08b2817ba | ||
![]() |
4ca13c409b | ||
![]() |
0d515e2303 | ||
![]() |
a2153bc6aa | ||
![]() |
ca171afe6f | ||
![]() |
bf4e97bd48 | ||
![]() |
8c59a12a03 | ||
![]() |
89569355be | ||
![]() |
3a41b3bdcf | ||
![]() |
12bd7037b3 | ||
![]() |
ca4f573be0 | ||
![]() |
07fceeab5a | ||
![]() |
3aa376e912 | ||
![]() |
92d30a8896 | ||
![]() |
83876fb9da | ||
![]() |
29bdf7877c | ||
![]() |
29199e2782 | ||
![]() |
68e1378615 | ||
![]() |
cf7efb5bfc | ||
![]() |
e8254f9aae | ||
![]() |
2e198af8c3 | ||
![]() |
ec36d396d9 | ||
![]() |
78914091b1 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"airbnb-typescript/base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/recommended",
|
||||
"prettier",
|
||||
@@ -45,16 +45,16 @@
|
||||
"func-names": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-var": 0,
|
||||
"strict": 0,
|
||||
"prefer-spread": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-bitwise": 0,
|
||||
"no-bitwise": 2,
|
||||
"comma-dangle": 0,
|
||||
"vars-on-top": 0,
|
||||
"no-continue": 0,
|
||||
"no-param-reassign": 0,
|
||||
"no-multi-assign": 0,
|
||||
"no-console": 2,
|
||||
"radix": 0,
|
||||
"no-alert": 0,
|
||||
"no-return-await": 0,
|
||||
|
@@ -147,6 +147,10 @@
|
||||
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
|
||||
"name": "mixcloud"
|
||||
},
|
||||
{
|
||||
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
|
||||
"name": "mixer"
|
||||
},
|
||||
{
|
||||
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
|
||||
"name": "nfc-off"
|
||||
|
@@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
|
||||
|
||||
@property() public viewPath?: string | number;
|
||||
|
||||
public urlPath?: string | null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
if (index === undefined) {
|
||||
@@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
|
@@ -87,6 +87,7 @@ export class HcMain extends HassElement {
|
||||
.hass=${this.hass}
|
||||
.lovelaceConfig=${this._lovelaceConfig}
|
||||
.viewPath=${this._lovelacePath}
|
||||
.urlPath=${this._urlPath}
|
||||
@config-refresh=${this._generateLovelaceConfig}
|
||||
></hc-lovelace>
|
||||
`;
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
@@ -19,13 +20,13 @@ import {
|
||||
HassioAddonRepository,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addon-repository";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
@@ -179,7 +180,7 @@ class HassioAddonStore extends LitElement {
|
||||
this._repos.sort(sortRepos);
|
||||
this._addons = addonsInfo.addons;
|
||||
} catch (err) {
|
||||
alert("Failed to fetch add-on info");
|
||||
alert(extractApiErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
|
||||
@customElement("hassio-addon-audio")
|
||||
class HassioAddonAudio extends LitElement {
|
||||
@@ -91,7 +92,9 @@ class HassioAddonAudio extends LitElement {
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
|
||||
<ha-progress-button @click=${this._saveSettings}>
|
||||
Save
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -172,7 +175,10 @@ class HassioAddonAudio extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSettings(): Promise<void> {
|
||||
private async _saveSettings(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
audio_input:
|
||||
@@ -182,12 +188,14 @@ class HassioAddonAudio extends LitElement {
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch {
|
||||
this._error = "Failed to set addon audio device";
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,14 +5,15 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
@@ -55,20 +57,103 @@ class HassioAddonConfig extends LitElement {
|
||||
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||
Reset to defaults
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || !valid}
|
||||
>
|
||||
Save
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
let data: HassioAddonSetOptionParams;
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -98,80 +183,6 @@ class HassioAddonConfig extends LitElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to reset addon configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveTapped(): Promise<void> {
|
||||
let data: HassioAddonSetOptionParams;
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to save addon configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -4,19 +4,21 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
@@ -85,38 +87,17 @@ class HassioAddonNetwork extends LitElement {
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||
Reset to defaults
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
|
||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||
Reset to defaults </ha-progress-button
|
||||
>>
|
||||
<ha-progress-button @click=${this._saveTapped}>
|
||||
Save
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
@@ -149,7 +130,10 @@ class HassioAddonNetwork extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
network: null,
|
||||
};
|
||||
@@ -162,17 +146,22 @@ class HassioAddonNetwork extends LitElement {
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon network configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _saveTapped(): Promise<void> {
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const networkconfiguration = {};
|
||||
this._config!.forEach((item) => {
|
||||
@@ -191,14 +180,38 @@ class HassioAddonNetwork extends LitElement {
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon network configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,18 +3,19 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import {
|
||||
fetchHassioAddonDocumentation,
|
||||
HassioAddonDetails,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
@@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
this.addon!.slug
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon documentation, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,21 +9,19 @@ import {
|
||||
mdiExclamationThick,
|
||||
mdiFlask,
|
||||
mdiHomeAssistant,
|
||||
mdiInformation,
|
||||
mdiKey,
|
||||
mdiNetwork,
|
||||
mdiPound,
|
||||
mdiShield,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
@@ -35,19 +33,27 @@ import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-label-badge";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
HassioAddonSetSecurityParams,
|
||||
installHassioAddon,
|
||||
setHassioAddonOption,
|
||||
setHassioAddonSecurity,
|
||||
startHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-card-content";
|
||||
@@ -127,8 +133,6 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@property({ type: Boolean }) private _installing = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._computeUpdateAvailable
|
||||
@@ -242,19 +246,23 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<div class="security">
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: this.addon.stage === "stable",
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
label="stage"
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
${this.addon.stage !== "stable"
|
||||
? html` <ha-label-badge
|
||||
class=${classMap({
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
label="stage"
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${STAGE_ICON[this.addon.stage]}
|
||||
></ha-svg-icon>
|
||||
</ha-label-badge>`
|
||||
: ""}
|
||||
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
@@ -382,67 +390,94 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
${this.addon.version
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Start on boot</div>
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Show in sidebar</div>
|
||||
<ha-switch
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
${this._computeCannotIngressSidebar
|
||||
? html`
|
||||
<span>
|
||||
This option requires Home Assistant 0.92 or
|
||||
later.
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._computeUsesProtectedOptions
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
|
||||
<paper-tooltip>
|
||||
Grant the add-on elevated system access.
|
||||
</paper-tooltip>
|
||||
<div class="addon-options">
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Start on boot
|
||||
</span>
|
||||
<span slot="description">
|
||||
Make the add-on start during a system boot
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
|
||||
${this.addon.startup !== "once"
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Watchdog
|
||||
</span>
|
||||
</div>
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<span slot="description">
|
||||
This will start the add-on if it crashes
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._watchdogToggled}
|
||||
.checked=${this.addon.watchdog}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Auto update
|
||||
</span>
|
||||
<span slot="description">
|
||||
Auto update the add-on when there is a new version
|
||||
available
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Show in sidebar
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this._computeCannotIngressSidebar
|
||||
? "This option requires Home Assistant 0.92 or later."
|
||||
: "Add this add-on to your sidebar"}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this._computeUsesProtectedOptions
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
Protection mode
|
||||
</span>
|
||||
<span slot="description">
|
||||
Blocks elevated system access from the add-on
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
@@ -468,12 +503,9 @@ class HassioAddonInfo extends LitElement {
|
||||
</ha-call-api-button>
|
||||
`
|
||||
: html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/start"
|
||||
>
|
||||
<ha-progress-button @click=${this._startClicked}>
|
||||
Start
|
||||
</ha-call-api-button>
|
||||
</ha-progress-button>
|
||||
`}
|
||||
${this._computeShowWebUI
|
||||
? html`
|
||||
@@ -497,12 +529,12 @@ class HassioAddonInfo extends LitElement {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
class=" right warning"
|
||||
@click=${this._uninstallClicked}
|
||||
>
|
||||
Uninstall
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
@@ -524,8 +556,7 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-progress-button
|
||||
.disabled=${!this.addon.available || this._installing}
|
||||
.progress=${this._installing}
|
||||
.disabled=${!this.addon.available}
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
Install
|
||||
@@ -548,137 +579,6 @@ class HassioAddonInfo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card.warning {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-header {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-content {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning mwc-button {
|
||||
--mdc-theme-primary: white !important;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.light-color {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
}
|
||||
.addon-version {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
img.logo {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.state {
|
||||
display: flex;
|
||||
margin: 33px 0;
|
||||
}
|
||||
.state div {
|
||||
width: 180px;
|
||||
display: inline-block;
|
||||
}
|
||||
.state ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
ha-svg-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
ha-svg-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
protection-enable mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
}
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.red {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
.blue {
|
||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||
}
|
||||
.green {
|
||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||
}
|
||||
.yellow {
|
||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||
}
|
||||
.security {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flow-root;
|
||||
}
|
||||
.security h3 {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
.changelog-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private get _computeHassioApi(): boolean {
|
||||
return (
|
||||
this.addon.hassio_api &&
|
||||
@@ -763,7 +663,29 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _watchdogToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
watchdog: !this.addon.watchdog,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +703,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,9 +723,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon security option, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,12 +743,13 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _openChangelog(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
@@ -835,15 +760,17 @@ class HassioAddonInfo extends LitElement {
|
||||
content,
|
||||
});
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon changelog, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get addon changelog",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _installClicked(): Promise<void> {
|
||||
this._error = undefined;
|
||||
this._installing = true;
|
||||
private async _installClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await installHassioAddon(this.hass, this.addon.slug);
|
||||
const eventdata = {
|
||||
@@ -853,12 +780,62 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to install addon, ${err.body?.message || err}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to install addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
this._installing = false;
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _uninstallClicked(): Promise<void> {
|
||||
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
try {
|
||||
const validate = await validateHassioAddonOption(
|
||||
this.hass,
|
||||
this.addon.slug
|
||||
);
|
||||
if (!validate.data.valid) {
|
||||
await showConfirmationDialog(this, {
|
||||
title: "Failed to start addon - configruation validation faled!",
|
||||
text: validate.data.message.split(" Got ")[0],
|
||||
confirm: () => this._openConfiguration(),
|
||||
confirmText: "Go to configruation",
|
||||
dismissText: "Cancel",
|
||||
});
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to validate addon configuration",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await startHassioAddon(this.hass, this.addon.slug);
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to start addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _openConfiguration(): void {
|
||||
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
|
||||
}
|
||||
|
||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to uninstall this add-on?",
|
||||
@@ -867,6 +844,7 @@ class HassioAddonInfo extends LitElement {
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -880,8 +858,152 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to uninstall addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card.warning {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-header {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning .card-content {
|
||||
color: white;
|
||||
}
|
||||
ha-card.warning mwc-button {
|
||||
--mdc-theme-primary: white !important;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.light-color {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
}
|
||||
.addon-version {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
img.logo {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
ha-svg-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
ha-svg-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
protection-enable mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
}
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.red {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
.blue {
|
||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||
}
|
||||
.green {
|
||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||
}
|
||||
.yellow {
|
||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||
}
|
||||
.security {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flow-root;
|
||||
}
|
||||
.security h3 {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
.changelog-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
.addon-options {
|
||||
max-width: 50%;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.addon-options {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
|
@@ -4,9 +4,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../src/components/ha-card";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
fetchHassioAddonLogs,
|
||||
HassioAddonDetails,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
@@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement {
|
||||
try {
|
||||
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
|
||||
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ interface State {
|
||||
class HassioAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`${this._parseTextToColoredPre(this.content)}`;
|
||||
}
|
||||
|
||||
|
@@ -5,19 +5,27 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
HassioResponse,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
@@ -126,31 +134,45 @@ export class HassioUpdate extends LitElement {
|
||||
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.path=${apiPath}
|
||||
@hass-api-called=${this._apiCalled}
|
||||
<ha-progress-button
|
||||
.apiPath=${apiPath}
|
||||
.name=${name}
|
||||
.version=${lastVersion}
|
||||
@click=${this._confirmUpdate}
|
||||
>
|
||||
Update
|
||||
</ha-call-api-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._error = "";
|
||||
private async _confirmUpdate(ev): Promise<void> {
|
||||
const item = ev.currentTarget;
|
||||
item.progress = true;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: `Update ${item.name}`,
|
||||
text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`,
|
||||
confirmText: "update",
|
||||
dismissText: "cancel",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
item.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
if (typeof response.body === "object") {
|
||||
this._error = response.body.message || "Unknown error";
|
||||
} else {
|
||||
this._error = response.body;
|
||||
try {
|
||||
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||
} catch (err) {
|
||||
// Only show an error if the status code was not 504, or no status at all (connection terminated)
|
||||
if (err.status_code && err.status_code !== 504) {
|
||||
showAlertDialog(this, {
|
||||
title: "Update failed",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
item.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -31,6 +31,10 @@ class HassioMarkdownDialog extends LitElement {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
@@ -38,7 +42,7 @@ class HassioMarkdownDialog extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this._closeDialog}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this.title)}
|
||||
>
|
||||
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
||||
@@ -46,10 +50,6 @@ class HassioMarkdownDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog(): void {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
|
333
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
333
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-related-items";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
NetworkInterface,
|
||||
updateNetworkInterface,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _prosessing = false;
|
||||
|
||||
@internalProperty() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@internalProperty() private _network!: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
}[];
|
||||
|
||||
@internalProperty() private _curTabIndex = 0;
|
||||
|
||||
@internalProperty() private _device?: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
};
|
||||
|
||||
@internalProperty() private _dirty = false;
|
||||
|
||||
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._dirty = false;
|
||||
this._curTabIndex = 0;
|
||||
this._network = Object.keys(params.network?.interfaces)
|
||||
.map((device) => ({
|
||||
interface: device,
|
||||
data: params.network.interfaces[device],
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
return a.data.primary > b.data.primary ? -1 : 1;
|
||||
});
|
||||
this._device = this._network[this._curTabIndex];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._prosessing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params || !this._network) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${true}
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
Network settings
|
||||
</span>
|
||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
${this._network.length > 1
|
||||
? html` <mwc-tab-bar
|
||||
.activeIndex=${this._curTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabActivated}
|
||||
>${this._network.map(
|
||||
(device) =>
|
||||
html`<mwc-tab
|
||||
.id=${device.interface}
|
||||
.label=${device.interface}
|
||||
>
|
||||
</mwc-tab>`
|
||||
)}
|
||||
</mwc-tab-bar>`
|
||||
: ""}
|
||||
</div>
|
||||
${cache(this._renderTab())}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTab() {
|
||||
return html` <div class="form container">
|
||||
<ha-formfield label="DHCP">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="dhcp"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "dhcp"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Static">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="static"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${this._device!.data.method !== "dhcp"
|
||||
? html` <paper-input
|
||||
class="flex-auto"
|
||||
id="ip_address"
|
||||
label="IP address/Netmask"
|
||||
.value="${this._device!.data.ip_address}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
label="Gateway address"
|
||||
.value="${this._device!.data.gateway}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
label="DNS servers"
|
||||
.value="${this._device!.data.nameservers as string}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
NB!: If you are changing IP or gateway addresses, you might lose
|
||||
the connection.`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
|
||||
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
|
||||
${this._prosessing
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: "Update"}
|
||||
</mwc-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._prosessing = true;
|
||||
let options: Partial<NetworkInterface> = {
|
||||
method: this._device!.data.method,
|
||||
};
|
||||
if (options.method !== "dhcp") {
|
||||
options = {
|
||||
...options,
|
||||
address: this._device!.data.ip_address,
|
||||
gateway: this._device!.data.gateway,
|
||||
dns: String(this._device!.data.nameservers).split(","),
|
||||
};
|
||||
}
|
||||
try {
|
||||
await updateNetworkInterface(this.hass, this._device!.interface, options);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to change network settings",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this._prosessing = false;
|
||||
return;
|
||||
}
|
||||
this._params?.loadData();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||
if (this._dirty) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text:
|
||||
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
|
||||
confirmText: "yes",
|
||||
dismissText: "no",
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_device");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = ev.detail.index;
|
||||
this._device = this._network[ev.detail.index];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as HaRadio).value as "dhcp" | "static";
|
||||
|
||||
if (!value || !this._device || this._device!.data.method === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
this._device!.data.method = value;
|
||||
this.requestUpdate("_device");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (!value || !this._device || this._device.data[id] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
this._device.data[id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-position: static;
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
.container {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
:host([rtl]) app-toolbar {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
.container {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 53px;
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-network": DialogHassioNetwork;
|
||||
}
|
||||
}
|
22
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
22
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { NetworkInfo } from "../../../../src/data/hassio/network";
|
||||
import "./dialog-hassio-network";
|
||||
|
||||
export interface HassioNetworkDialogParams {
|
||||
network: NetworkInfo;
|
||||
loadData: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showNetworkDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioNetworkDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-network",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -5,25 +5,26 @@ import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
@@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
input.value = "";
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
this._prosessing = false;
|
||||
}
|
||||
@@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
await this._dialogParams!.loadData();
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,18 +7,20 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioSnapshotInfo,
|
||||
HassioSnapshotDetail,
|
||||
} from "../../../../src/data/hassio/snapshot";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
@@ -266,8 +268,12 @@ class HassioSnapshotDialog extends LitElement {
|
||||
this._snapshotPassword = ev.detail.value;
|
||||
}
|
||||
|
||||
private _partialRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
private async _partialRestoreClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want partially to restore this snapshot?",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,8 +318,13 @@ class HassioSnapshotDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _fullRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
private async _fullRestoreClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title:
|
||||
"Are you sure you want to wipe your system and restore this snapshot?",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,8 +349,12 @@ class HassioSnapshotDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _deleteClicked() {
|
||||
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
||||
private async _deleteClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want to delete this snapshot?",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -365,7 +380,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
||||
);
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
alert(`Error: ${extractApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
HassioAddonDetails,
|
||||
restartHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -26,7 +27,7 @@ export const suggestAddonRestart = async (
|
||||
} catch (err) {
|
||||
showAlertDialog(element, {
|
||||
title: "Failed to restart",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,116 +1,35 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
html,
|
||||
PropertyValues,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "./hassio-router";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import {
|
||||
createHassioSession,
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
HassioInfo,
|
||||
HassioPanelInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import {
|
||||
AlertDialogParams,
|
||||
showAlertDialog,
|
||||
} from "../../src/dialogs/generic/show-dialog-box";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
|
||||
@customElement("hassio-main")
|
||||
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
initialLoad: () => this._fetchData(),
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||
@property() public route?: Route;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
||||
);
|
||||
this._applyTheme();
|
||||
|
||||
this.style.setProperty(
|
||||
"--app-header-background-color",
|
||||
"var(--sidebar-background-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-text-color",
|
||||
"var(--sidebar-text-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-border-bottom",
|
||||
"1px solid var(--divider-color)"
|
||||
);
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
// supervisor UI can also run under older versions of Home Assistant.
|
||||
@@ -143,152 +62,61 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, document.body);
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
if (oldHass.themes !== this.hass.themes) {
|
||||
this._applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
(el as PolymerElement).setProperties({
|
||||
hass: this.hass,
|
||||
narrow: this.narrow,
|
||||
supervisorInfo: this._supervisorInfo,
|
||||
hassioInfo: this._hassioInfo,
|
||||
hostInfo: this._hostInfo,
|
||||
hassInfo: this._hassInfo,
|
||||
hassOsInfo: this._hassOsInfo,
|
||||
route,
|
||||
});
|
||||
protected render() {
|
||||
return html`
|
||||
<hassio-router
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-router>
|
||||
`;
|
||||
}
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
dark: this.hass.themes.darkMode,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hassioInfo = this._hassioInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.hassOsInfo = this._hassOsInfo;
|
||||
el.route = route;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
await this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
themeName =
|
||||
((this.hass.selectedTheme as unknown) as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
]);
|
||||
this._supervisorInfo = supervisorInfo;
|
||||
this._hassioInfo = hassioInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
this._hassInfo = hassInfo;
|
||||
|
||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private async _redirectIngress(addonSlug: string) {
|
||||
// When we trigger a navigation, we sleep to make sure we don't
|
||||
// show the hassio dashboard before navigating away.
|
||||
const awaitAlert = async (
|
||||
alertParams: AlertDialogParams,
|
||||
action: () => void
|
||||
) => {
|
||||
await new Promise((resolve) => {
|
||||
alertParams.confirm = resolve;
|
||||
showAlertDialog(this, alertParams);
|
||||
});
|
||||
action();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
};
|
||||
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
);
|
||||
|
||||
let addon;
|
||||
|
||||
try {
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
},
|
||||
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,8 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
@@ -33,6 +35,9 @@ class HassioPanel extends LitElement {
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorInfo) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.route=${this.route}
|
||||
@@ -46,6 +51,16 @@ class HassioPanel extends LitElement {
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
150
hassio/src/hassio-router.ts
Normal file
150
hassio/src/hassio-router.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
HassioInfo,
|
||||
HassioPanelInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
|
||||
@customElement("hassio-router")
|
||||
class HassioRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
initialLoad: () => this._fetchData(),
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hassioInfo = this._hassioInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.hassOsInfo = this._hassOsInfo;
|
||||
el.route = route;
|
||||
|
||||
if (el.localName === "hassio-ingress-view") {
|
||||
el.ingressPanel = this.panel.config && this.panel.config.ingress;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
]);
|
||||
this._supervisorInfo = supervisorInfo;
|
||||
this._hassioInfo = hassioInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
this._hassInfo = hassInfo;
|
||||
|
||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private _redirectIngress(addonSlug: string) {
|
||||
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-router": HassioRouter;
|
||||
}
|
||||
}
|
@@ -17,6 +17,10 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("hassio-ingress-view")
|
||||
class HassioIngressView extends LitElement {
|
||||
@@ -24,22 +28,45 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() public ingressPanel = false;
|
||||
|
||||
@internalProperty() private _addon?: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public narrow = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage .header=${this._addon.name} hassio>
|
||||
<iframe src=${this._addon.ingress_url}></iframe>
|
||||
</hass-subpage>
|
||||
`;
|
||||
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
.header=${this._addon.name}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${iframe}
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
|
||||
? html`<div class="header">
|
||||
<mwc-icon-button
|
||||
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@click=${this._toggleMenu}
|
||||
>
|
||||
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<div class="main-title">${this._addon.name}</div>
|
||||
</div>
|
||||
${iframe}`
|
||||
: iframe}`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
super.updated(changedProps);
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
return;
|
||||
@@ -56,27 +83,56 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
|
||||
let addon;
|
||||
|
||||
try {
|
||||
const [addon] = await Promise.all([
|
||||
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
|
||||
throw new Error("Failed to fetch add-on info");
|
||||
}),
|
||||
createHassioSession(this.hass).catch(() => {
|
||||
throw new Error("Failed to create an ingress session");
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!addon.ingress) {
|
||||
throw new Error("This add-on does not support ingress");
|
||||
}
|
||||
|
||||
this._addon = addon;
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
alert(err.message || "Unknown error starting ingress.");
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
});
|
||||
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -87,6 +143,41 @@ class HassioIngressView extends LitElement {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.header + iframe {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
mwc-icon-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -13,15 +13,17 @@ import {
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
createHassioFullSnapshot,
|
||||
createHassioPartialSnapshot,
|
||||
@@ -80,8 +82,6 @@ class HassioSnapshots extends LitElement {
|
||||
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
||||
];
|
||||
|
||||
@internalProperty() private _creatingSnapshot = false;
|
||||
|
||||
@internalProperty() private _error = "";
|
||||
|
||||
public async refreshData() {
|
||||
@@ -192,12 +192,9 @@ class HassioSnapshots extends LitElement {
|
||||
: undefined}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
<ha-progress-button @click=${this._createSnapshot}>
|
||||
Create
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
@@ -230,7 +227,7 @@ class HassioSnapshots extends LitElement {
|
||||
.icon=${snapshot.type === "full"
|
||||
? mdiPackageVariantClosed
|
||||
: mdiPackageVariant}
|
||||
.icon-class="snapshot"
|
||||
icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -293,17 +290,20 @@ class HassioSnapshots extends LitElement {
|
||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
||||
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSnapshot() {
|
||||
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = "";
|
||||
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||
this._error = "Please enter a password.";
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
this._creatingSnapshot = true;
|
||||
await this.updateComplete;
|
||||
|
||||
const name =
|
||||
@@ -343,10 +343,9 @@ class HassioSnapshots extends LitElement {
|
||||
this._updateSnapshots();
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
this._creatingSnapshot = false;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _computeDetails(snapshot: HassioSnapshot) {
|
||||
|
@@ -1,18 +1,29 @@
|
||||
import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { safeDump } from "js-yaml";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
changeHostOptions,
|
||||
configSyncOS,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo as HassioHostInfoType,
|
||||
@@ -20,6 +31,10 @@ import {
|
||||
shutdownHost,
|
||||
updateOS,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchNetworkInfo,
|
||||
NetworkInfo,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import { HassioInfo } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -29,6 +44,7 @@ import {
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
@@ -41,164 +57,179 @@ class HassioHostInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _errors?: string;
|
||||
@internalProperty() public _networkInfo?: NetworkInfo;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
protected render(): TemplateResult | void {
|
||||
const primaryIpAddress = this.hostInfo.features.includes("network")
|
||||
? this._primaryIpAddress(this._networkInfo!)
|
||||
: "";
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-card header="Host System">
|
||||
<div class="card-content">
|
||||
<h2>Host system</h2>
|
||||
<table class="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hostname</td>
|
||||
<td>${this.hostInfo.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System</td>
|
||||
<td>${this.hostInfo.operating_system}</td>
|
||||
</tr>
|
||||
${!this.hostInfo.features.includes("hassos")
|
||||
? html`<tr>
|
||||
<td>Docker version</td>
|
||||
<td>${this.hassioInfo.docker}</td>
|
||||
</tr>`
|
||||
: ""}
|
||||
${this.hostInfo.deployment
|
||||
? html`
|
||||
<tr>
|
||||
<td>Deployment</td>
|
||||
<td>${this.hostInfo.deployment}</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
<mwc-button raised @click=${this._showHardware} class="info">
|
||||
Hardware
|
||||
</mwc-button>
|
||||
${this.hostInfo.features.includes("hostname")
|
||||
? html`
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Hostname
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.hostname}
|
||||
</span>
|
||||
<mwc-button
|
||||
raised
|
||||
title="Change the hostname"
|
||||
label="Change"
|
||||
@click=${this._changeHostnameClicked}
|
||||
class="info"
|
||||
>
|
||||
Change hostname
|
||||
</mwc-button>
|
||||
`
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this._errors
|
||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
||||
${this.hostInfo.features.includes("network") &&
|
||||
atLeastVersion(this.hass.config.version, 0, 115)
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
IP address
|
||||
</span>
|
||||
<span slot="description">
|
||||
${primaryIpAddress}
|
||||
</span>
|
||||
<mwc-button
|
||||
title="Change the network"
|
||||
label="Change"
|
||||
@click=${this._changeNetworkClicked}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Operating system
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.operating_system}
|
||||
</span>
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest &&
|
||||
this.hostInfo.features.includes("hassos")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the host OS"
|
||||
@click=${this._osUpdate}
|
||||
>
|
||||
Update
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
${!this.hostInfo.features.includes("hassos")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Docker version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hassioInfo.docker}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.hostInfo.deployment
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Deployment
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.deployment}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.hostInfo.features.includes("reboot")
|
||||
? html`
|
||||
<mwc-button class="warning" @click=${this._rebootHost}
|
||||
>Reboot</mwc-button
|
||||
<ha-progress-button
|
||||
title="Reboot the host OS"
|
||||
class="warning"
|
||||
@click=${this._hostReboot}
|
||||
>
|
||||
Reboot
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("shutdown")
|
||||
? html`
|
||||
<mwc-button class="warning" @click=${this._shutdownHost}
|
||||
>Shutdown</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("hassos")
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
<ha-progress-button
|
||||
title="Shutdown the host OS"
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/os/config/sync"
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>Import from USB</ha-call-api-button
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
Shutdown
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
||||
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
|
||||
: ""}
|
||||
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleMenuAction}
|
||||
>
|
||||
<mwc-icon-button slot="trigger">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item title="Show a list of hardware">
|
||||
Hardware
|
||||
</mwc-list-item>
|
||||
${this.hostInfo.features.includes("hassos")
|
||||
? html`<mwc-list-item
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>
|
||||
Import from USB
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
mwc-button.info {
|
||||
max-width: calc(50% - 12px);
|
||||
}
|
||||
table.info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._errors = undefined;
|
||||
return;
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info) {
|
||||
return "";
|
||||
}
|
||||
return Object.keys(network_info?.interfaces)
|
||||
.map((device) => network_info.interfaces[device])
|
||||
.find((device) => device.primary)?.ip_address;
|
||||
});
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
this._errors =
|
||||
typeof response.body === "object"
|
||||
? response.body.message || "Unknown error"
|
||||
: response.body;
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._showHardware();
|
||||
break;
|
||||
case 1:
|
||||
await this._importFromUSB();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
try {
|
||||
const content = this._objectToMarkdown(
|
||||
await fetchHassioHardwareInfo(this.hass)
|
||||
);
|
||||
const content = await fetchHassioHardwareInfo(this.hass);
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content,
|
||||
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||
});
|
||||
} catch (err) {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: "Error getting hardware info",
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get Hardware list",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _rebootHost(): Promise<void> {
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to reboot the host?",
|
||||
@@ -207,6 +238,7 @@ class HassioHostInfo extends LitElement {
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,12 +247,16 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reboot",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _shutdownHost(): Promise<void> {
|
||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Shutdown",
|
||||
text: "Are you sure you want to shutdown the host?",
|
||||
@@ -229,6 +265,7 @@ class HassioHostInfo extends LitElement {
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,12 +274,16 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to shutdown",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _updateOS(): Promise<void> {
|
||||
private async _osUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update",
|
||||
text: "Are you sure you want to update the OS?",
|
||||
@@ -251,6 +292,7 @@ class HassioHostInfo extends LitElement {
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -259,30 +301,17 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _objectToMarkdown(obj, indent = ""): string {
|
||||
let data = "";
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] !== "object") {
|
||||
data += `${indent}- ${key}: ${obj[key]}\n`;
|
||||
} else {
|
||||
data += `${indent}- ${key}:\n`;
|
||||
if (Array.isArray(obj[key])) {
|
||||
if (obj[key].length) {
|
||||
data +=
|
||||
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
|
||||
}
|
||||
} else {
|
||||
data += this._objectToMarkdown(obj[key], ` ${indent}`);
|
||||
}
|
||||
}
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
network: this._networkInfo!,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
@@ -301,11 +330,83 @@ class HassioHostInfo extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to import from USB",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._networkInfo = await fetchNetworkInfo(this.hass);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
@media (min-width: 563px) {
|
||||
paper-listbox {
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
min-height: 35px;
|
||||
}
|
||||
mwc-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -6,21 +5,28 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@@ -28,75 +34,232 @@ class HassioSupervisorInfo extends LitElement {
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
||||
|
||||
@internalProperty() private _errors?: string;
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-card header="Supervisor">
|
||||
<div class="card-content">
|
||||
<h2>Supervisor</h2>
|
||||
<table class="info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>${this.supervisorInfo.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
<td>${this.supervisorInfo.version_latest}</td>
|
||||
</tr>
|
||||
${this.supervisorInfo.channel !== "stable"
|
||||
? html`
|
||||
<tr>
|
||||
<td>Channel</td>
|
||||
<td>${this.supervisorInfo.channel}</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
${this._errors
|
||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
||||
: ""}
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Newest version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version_latest}
|
||||
</span>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the supervisor"
|
||||
@click=${this._supervisorUpdate}
|
||||
>
|
||||
Update
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Channel
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.channel}
|
||||
</span>
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
title="Get stable updates for Home Assistant, supervisor and host"
|
||||
>
|
||||
Leave beta channel
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisorInfo.channel === "stable"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>
|
||||
Join beta channel
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisorInfo?.supported
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
Share diagnostics
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
Share crash reports and diagnostic information.
|
||||
<button
|
||||
class="link"
|
||||
title="Show more information about this"
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisorInfo.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: html`<div class="error">
|
||||
You are running an unsupported installation.
|
||||
<a
|
||||
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
|
||||
"hassos"
|
||||
)
|
||||
? "0015-home-assistant-os.md"
|
||||
: "0014-home-assistant-supervised.md"}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Learn more about how you can make your system compliant"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
|
||||
>Reload</ha-call-api-button
|
||||
<ha-progress-button
|
||||
@click=${this._supervisorReload}
|
||||
title="Reload parts of the supervisor."
|
||||
>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/supervisor/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/supervisor/options"
|
||||
.data=${{ channel: "stable" }}
|
||||
>Leave beta channel</ha-call-api-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisorInfo.channel === "stable"
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._joinBeta}
|
||||
class="warning"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>Join beta channel</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
Reload
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _toggleBeta(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
if (this.supervisorInfo.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
contain unstable code changes.
|
||||
<br />
|
||||
<b>
|
||||
Make sure you have backups of your data before you activate this
|
||||
feature.
|
||||
</b>
|
||||
<br /><br />
|
||||
This includes beta releases for:
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<br />
|
||||
Do you want to join the beta channel?`,
|
||||
confirmText: "join beta",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reload the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update supervisor",
|
||||
text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`,
|
||||
confirmText: "update",
|
||||
dismissText: "cancel",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: "Help Improve Home Assistant",
|
||||
text: html`Would you want to automatically share crash reports and
|
||||
diagnostic information when the supervisor encounters unexpected errors?
|
||||
<br /><br />
|
||||
This will allow us to fix the problems, the information is only
|
||||
accessible to the Home Assistant Core team and will not be shared with
|
||||
others.
|
||||
<br /><br />
|
||||
The data does not include any private/sensitive information and you can
|
||||
disable this in settings at any time you want.`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -104,83 +267,35 @@ class HassioSupervisorInfo extends LitElement {
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private _apiCalled(ev): void {
|
||||
if (ev.detail.success) {
|
||||
this._errors = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = ev.detail.response;
|
||||
|
||||
this._errors =
|
||||
typeof response.body === "object"
|
||||
? response.body.message || "Unknown error"
|
||||
: response.body;
|
||||
}
|
||||
|
||||
private async _joinBeta() {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
contain unstable code changes.
|
||||
<br />
|
||||
<b>
|
||||
Make sure you have backups of your data before you activate this
|
||||
feature.
|
||||
</b>
|
||||
<br /><br />
|
||||
This includes beta releases for:
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<br />
|
||||
Do you want to join the beta channel?`,
|
||||
confirmText: "join beta",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data: SupervisorOptions = { channel: "beta" };
|
||||
await setSupervisorOption(this.hass, data);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -7,12 +7,14 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -67,7 +69,7 @@ class HassioSupervisorLog extends LitElement {
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
@@ -102,18 +104,49 @@ class HassioSupervisorLog extends LitElement {
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
||||
<ha-progress-button @click=${this._refresh}>
|
||||
Refresh
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
await this._loadData();
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
@@ -127,38 +160,9 @@ class HassioSupervisorLog extends LitElement {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-content {
|
||||
padding-top: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
this._content = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _refresh(): Promise<void> {
|
||||
await this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -12,8 +12,8 @@ import {
|
||||
HassioHostInfo,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioSupervisorInfo,
|
||||
HassioInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
|
||||
>
|
||||
<span slot="header">System</span>
|
||||
<div class="content">
|
||||
<h1>Information</h1>
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
@@ -65,7 +65,6 @@ class HassioSystem extends LitElement {
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<h1>System log</h1>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
|
62
package.json
62
package.json
@@ -23,25 +23,29 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||
"@fullcalendar/core": "^5.0.0-beta.2",
|
||||
"@fullcalendar/daygrid": "^5.0.0-beta.2",
|
||||
"@material/chips": "=8.0.0-canary.a78ceb112.0",
|
||||
"@fullcalendar/common": "5.1.0",
|
||||
"@fullcalendar/core": "5.1.0",
|
||||
"@fullcalendar/daygrid": "5.1.0",
|
||||
"@fullcalendar/interaction": "5.1.0",
|
||||
"@fullcalendar/list": "5.1.0",
|
||||
"@material/chips": "=8.0.0-canary.096a7a066.0",
|
||||
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
||||
"@material/mwc-button": "^0.17.2",
|
||||
"@material/mwc-checkbox": "^0.17.2",
|
||||
"@material/mwc-dialog": "^0.17.2",
|
||||
"@material/mwc-fab": "^0.17.2",
|
||||
"@material/mwc-formfield": "^0.17.2",
|
||||
"@material/mwc-icon-button": "^0.17.2",
|
||||
"@material/mwc-list": "^0.17.2",
|
||||
"@material/mwc-menu": "^0.17.2",
|
||||
"@material/mwc-ripple": "^0.17.2",
|
||||
"@material/mwc-switch": "^0.17.2",
|
||||
"@material/mwc-tab": "^0.17.2",
|
||||
"@material/mwc-tab-bar": "^0.17.2",
|
||||
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
|
||||
"@mdi/js": "5.3.45",
|
||||
"@mdi/svg": "5.3.45",
|
||||
"@material/mwc-button": "^0.18.0",
|
||||
"@material/mwc-checkbox": "^0.18.0",
|
||||
"@material/mwc-dialog": "^0.18.0",
|
||||
"@material/mwc-fab": "^0.18.0",
|
||||
"@material/mwc-formfield": "^0.18.0",
|
||||
"@material/mwc-icon-button": "^0.18.0",
|
||||
"@material/mwc-list": "^0.18.0",
|
||||
"@material/mwc-menu": "^0.18.0",
|
||||
"@material/mwc-radio": "^0.18.0",
|
||||
"@material/mwc-ripple": "^0.18.0",
|
||||
"@material/mwc-switch": "^0.18.0",
|
||||
"@material/mwc-tab": "^0.18.0",
|
||||
"@material/mwc-tab-bar": "^0.18.0",
|
||||
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
|
||||
"@mdi/js": "5.5.55",
|
||||
"@mdi/svg": "5.5.55",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
@@ -84,6 +88,7 @@
|
||||
"codemirror": "^5.49.0",
|
||||
"comlink": "^4.3.0",
|
||||
"cpx": "^1.5.0",
|
||||
"cropperjs": "^1.5.7",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
@@ -100,14 +105,16 @@
|
||||
"lit-element": "^2.3.1",
|
||||
"lit-html": "^1.2.1",
|
||||
"lit-virtualizer": "^0.4.2",
|
||||
"marked": "^0.6.1",
|
||||
"marked": "^1.1.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"node-vibrant": "^3.1.5",
|
||||
"proxy-polyfill": "^0.3.1",
|
||||
"punycode": "^2.1.1",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sortablejs": "^1.10.2",
|
||||
"superstruct": "^0.10.12",
|
||||
"unfetch": "^4.1.0",
|
||||
"vue": "^2.6.11",
|
||||
@@ -136,13 +143,14 @@
|
||||
"@rollup/plugin-replace": "^2.3.2",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/codemirror": "^0.0.78",
|
||||
"@types/codemirror": "^0.0.97",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
"@types/marked": "^1.1.0",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
||||
@@ -153,7 +161,7 @@
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.2",
|
||||
"eslint-plugin-disable": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-lit": "^1.2.0",
|
||||
@@ -176,7 +184,7 @@
|
||||
"magic-string": "^0.25.7",
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^6.0.2",
|
||||
"mocha": "^7.2.0",
|
||||
"object-hash": "^2.0.3",
|
||||
"open": "^7.0.4",
|
||||
"prettier": "^2.0.4",
|
||||
@@ -193,7 +201,7 @@
|
||||
"systemjs": "^6.3.2",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"ts-lit-plugin": "^1.2.0",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^3.8.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
@@ -210,7 +218,11 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"lit-html": "1.2.1",
|
||||
"lit-element": "2.3.1"
|
||||
"lit-element": "2.3.1",
|
||||
"@material/animation": "8.0.0-canary.096a7a066.0",
|
||||
"@material/base": "8.0.0-canary.096a7a066.0",
|
||||
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
|
||||
"@material/theme": "8.0.0-canary.096a7a066.0"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200716.0",
|
||||
version="20200904.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -16,6 +16,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./ha-auth-flow";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import punycode from "punycode";
|
||||
|
||||
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
|
||||
|
||||
@@ -75,7 +76,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
${this.localize(
|
||||
"ui.panel.page-authorize.authorizing_client",
|
||||
"clientId",
|
||||
this.clientId
|
||||
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
|
||||
)}
|
||||
</p>
|
||||
${loggingInWith}
|
||||
|
113
src/common/color/convert-color.ts
Normal file
113
src/common/color/convert-color.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
const expand_hex = (hex: string): string => {
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return [
|
||||
parseInt(hex.substring(0, 2), 16),
|
||||
parseInt(hex.substring(2, 4), 16),
|
||||
parseInt(hex.substring(4, 6), 16),
|
||||
];
|
||||
};
|
||||
|
||||
export const rgb2hex = (rgb: [number, number, number]): string => {
|
||||
return `#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
|
||||
};
|
||||
|
||||
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
// Constants for XYZ and LAB conversion
|
||||
const Xn = 0.95047;
|
||||
const Yn = 1;
|
||||
const Zn = 1.08883;
|
||||
|
||||
const t0 = 0.137931034; // 4 / 29
|
||||
const t1 = 0.206896552; // 6 / 29
|
||||
const t2 = 0.12841855; // 3 * t1 * t1
|
||||
const t3 = 0.008856452; // t1 * t1 * t1
|
||||
|
||||
const rgb_xyz = (r: number) => {
|
||||
r /= 255;
|
||||
if (r <= 0.04045) {
|
||||
return r / 12.92;
|
||||
}
|
||||
return ((r + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
|
||||
const xyz_lab = (t: number) => {
|
||||
if (t > t3) {
|
||||
return t ** (1 / 3);
|
||||
}
|
||||
return t / t2 + t0;
|
||||
};
|
||||
|
||||
const xyz_rgb = (r: number) => {
|
||||
return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
|
||||
};
|
||||
|
||||
const lab_xyz = (t: number) => {
|
||||
return t > t1 ? t * t * t : t2 * (t - t0);
|
||||
};
|
||||
|
||||
// Conversions between RGB and LAB
|
||||
|
||||
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
|
||||
let [r, g, b] = rgb;
|
||||
r = rgb_xyz(r);
|
||||
g = rgb_xyz(g);
|
||||
b = rgb_xyz(b);
|
||||
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
|
||||
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
|
||||
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
|
||||
return [x, y, z];
|
||||
};
|
||||
|
||||
export const rgb2lab = (
|
||||
rgb: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [x, y, z] = rgb2xyz(rgb);
|
||||
const l = 116 * y - 16;
|
||||
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
|
||||
};
|
||||
|
||||
export const lab2rgb = (
|
||||
lab: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [l, a, b] = lab;
|
||||
|
||||
let y = (l + 16) / 116;
|
||||
let x = isNaN(a) ? y : y + a / 500;
|
||||
let z = isNaN(b) ? y : y - b / 200;
|
||||
|
||||
y = Yn * lab_xyz(y);
|
||||
x = Xn * lab_xyz(x);
|
||||
z = Zn * lab_xyz(z);
|
||||
|
||||
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
|
||||
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
|
||||
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
|
||||
|
||||
return [r, g, b_];
|
||||
};
|
||||
|
||||
export const lab2hex = (lab: [number, number, number]): string => {
|
||||
const rgb = lab2rgb(lab);
|
||||
return rgb2hex(rgb);
|
||||
};
|
16
src/common/color/lab.ts
Normal file
16
src/common/color/lab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// From https://github.com/gka/chroma.js
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
export const labDarken = (
|
||||
lab: [number, number, number],
|
||||
amount = 1
|
||||
): [number, number, number] => {
|
||||
return [lab[0] - 18 * amount, lab[1], lab[2]];
|
||||
};
|
||||
|
||||
export const labBrighten = (
|
||||
lab: [number, number, number],
|
||||
amount = 1
|
||||
): [number, number, number] => {
|
||||
return labDarken(lab, -amount);
|
||||
};
|
24
src/common/color/rgb.ts
Normal file
24
src/common/color/rgb.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const luminosity = (rgb: [number, number, number]): number => {
|
||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
const lum: [number, number, number] = [0, 0, 0];
|
||||
for (let i = 0; i < rgb.length; i++) {
|
||||
const chan = rgb[i] / 255;
|
||||
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
|
||||
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||||
};
|
||||
|
||||
export const rgbContrast = (
|
||||
color1: [number, number, number],
|
||||
color2: [number, number, number]
|
||||
) => {
|
||||
const lum1 = luminosity(color1);
|
||||
const lum2 = luminosity(color2);
|
||||
|
||||
if (lum1 > lum2) {
|
||||
return (lum1 + 0.05) / (lum2 + 0.05);
|
||||
}
|
||||
|
||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||
};
|
9
src/common/config/is_service_loaded.ts
Normal file
9
src/common/config/is_service_loaded.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a service is loaded. */
|
||||
export const isServiceLoaded = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
service: string
|
||||
): boolean =>
|
||||
hass && domain in hass.services && service in hass.services[domain];
|
@@ -21,6 +21,11 @@ export default function relativeTime(
|
||||
const tense = delta >= 0 ? "past" : "future";
|
||||
delta = Math.abs(delta);
|
||||
let roundedDelta = Math.round(delta);
|
||||
|
||||
if (roundedDelta === 0) {
|
||||
return localize("ui.components.relative_time.just_now");
|
||||
}
|
||||
|
||||
let unit = "week";
|
||||
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
|
155
src/common/decorators/local-storage.ts
Normal file
155
src/common/decorators/local-storage.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { PropertyDeclaration, UpdatingElement } from "lit-element";
|
||||
import type { ClassElement } from "../../types";
|
||||
|
||||
type Callback = (oldValue: any, newValue: any) => void;
|
||||
|
||||
class Storage {
|
||||
constructor() {
|
||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||
if (ev.key && this.hasKey(ev.key)) {
|
||||
this._storage[ev.key] = ev.newValue
|
||||
? JSON.parse(ev.newValue)
|
||||
: ev.newValue;
|
||||
if (this._listeners[ev.key]) {
|
||||
this._listeners[ev.key].forEach((listener) =>
|
||||
listener(
|
||||
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
|
||||
this._storage[ev.key!]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _storage: { [storageKey: string]: any } = {};
|
||||
|
||||
private _listeners: {
|
||||
[storageKey: string]: Callback[];
|
||||
} = {};
|
||||
|
||||
public addFromStorage(storageKey: any): void {
|
||||
if (!this._storage[storageKey]) {
|
||||
const data = window.localStorage.getItem(storageKey);
|
||||
if (data) {
|
||||
this._storage[storageKey] = JSON.parse(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeChanges(
|
||||
storageKey: string,
|
||||
callback: Callback
|
||||
): UnsubscribeFunc {
|
||||
if (this._listeners[storageKey]) {
|
||||
this._listeners[storageKey].push(callback);
|
||||
} else {
|
||||
this._listeners[storageKey] = [callback];
|
||||
}
|
||||
return () => {
|
||||
this.unsubscribeChanges(storageKey, callback);
|
||||
};
|
||||
}
|
||||
|
||||
public unsubscribeChanges(storageKey: string, callback: Callback) {
|
||||
if (!(storageKey in this._listeners)) {
|
||||
return;
|
||||
}
|
||||
const index = this._listeners[storageKey].indexOf(callback);
|
||||
if (index !== -1) {
|
||||
this._listeners[storageKey].splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public hasKey(storageKey: string): any {
|
||||
return storageKey in this._storage;
|
||||
}
|
||||
|
||||
public getValue(storageKey: string): any {
|
||||
return this._storage[storageKey];
|
||||
}
|
||||
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
// Safari in private mode doesn't allow localstorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export const LocalStorage = (
|
||||
storageKey?: string,
|
||||
property?: boolean,
|
||||
propertyOptions?: PropertyDeclaration
|
||||
): any => {
|
||||
return (clsElement: ClassElement) => {
|
||||
const key = String(clsElement.key);
|
||||
storageKey = storageKey || String(clsElement.key);
|
||||
const initVal = clsElement.initializer
|
||||
? clsElement.initializer()
|
||||
: undefined;
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
|
||||
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
|
||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
});
|
||||
|
||||
const getValue = (): any => {
|
||||
return storage.hasKey(storageKey!)
|
||||
? storage.getValue(storageKey!)
|
||||
: initVal;
|
||||
};
|
||||
|
||||
const setValue = (el: UpdatingElement, value: any) => {
|
||||
let oldValue: unknown | undefined;
|
||||
if (property) {
|
||||
oldValue = getValue();
|
||||
}
|
||||
storage.setValue(storageKey!, value);
|
||||
if (property) {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
kind: "method",
|
||||
placement: "prototype",
|
||||
key: clsElement.key,
|
||||
descriptor: {
|
||||
set(this: UpdatingElement, value: unknown) {
|
||||
setValue(this, value);
|
||||
},
|
||||
get() {
|
||||
return getValue();
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
finisher(cls: typeof UpdatingElement) {
|
||||
if (property) {
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribe(this);
|
||||
};
|
||||
cls.prototype.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`]();
|
||||
};
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
...propertyOptions,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
@@ -1,26 +1,20 @@
|
||||
import { derivedStyles } from "../../resources/styles";
|
||||
import { derivedStyles, darkStyles } from "../../resources/styles";
|
||||
import { HomeAssistant, Theme } from "../../types";
|
||||
import {
|
||||
hex2rgb,
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
lab2rgb,
|
||||
lab2hex,
|
||||
} from "../color/convert-color";
|
||||
import { rgbContrast } from "../color/rgb";
|
||||
import { labDarken, labBrighten } from "../color/lab";
|
||||
|
||||
interface ProcessedTheme {
|
||||
keys: { [key: string]: "" };
|
||||
styles: { [key: string]: string };
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): string | null => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(checkHex);
|
||||
return result
|
||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
||||
result[3],
|
||||
16
|
||||
)}`
|
||||
: null;
|
||||
};
|
||||
|
||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
|
||||
/**
|
||||
@@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string
|
||||
selectedTheme?: string,
|
||||
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||
) => {
|
||||
const newTheme = selectedTheme
|
||||
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
|
||||
: undefined;
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<Theme> = {};
|
||||
|
||||
if (!element._themes && !newTheme) {
|
||||
if (selectedTheme === "default" && themeOptions) {
|
||||
if (themeOptions.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = darkStyles;
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (themeOptions.accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||
themeRules["accent-color"] = themeOptions.accentColor;
|
||||
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
}
|
||||
|
||||
if (!element._themes && !Object.keys(themeRules).length) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
|
||||
const newTheme =
|
||||
themeRules && cacheKey
|
||||
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
|
||||
: undefined;
|
||||
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
@@ -58,42 +91,45 @@ export const applyThemesOnElement = (
|
||||
};
|
||||
|
||||
const processTheme = (
|
||||
themeName: string,
|
||||
themes: HomeAssistant["themes"]
|
||||
cacheKey: string,
|
||||
theme: Partial<Theme>
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!themes.themes[themeName]) {
|
||||
if (!theme || !Object.keys(theme).length) {
|
||||
return undefined;
|
||||
}
|
||||
const theme: Theme = {
|
||||
const combinedTheme: Partial<Theme> = {
|
||||
...derivedStyles,
|
||||
...themes.themes[themeName],
|
||||
...theme,
|
||||
};
|
||||
const styles = {};
|
||||
const keys = {};
|
||||
for (const key of Object.keys(theme)) {
|
||||
for (const key of Object.keys(combinedTheme)) {
|
||||
const prefixedKey = `--${key}`;
|
||||
const value = theme[key];
|
||||
const value = combinedTheme[key]!;
|
||||
styles[prefixedKey] = value;
|
||||
keys[prefixedKey] = "";
|
||||
|
||||
// Try to create a rgb value for this key if it is a hex color
|
||||
// Try to create a rgb value for this key if it is not a var
|
||||
if (!value.startsWith("#")) {
|
||||
// Not a hex color
|
||||
// Can't convert non hex value
|
||||
continue;
|
||||
}
|
||||
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
if (combinedTheme[rgbKey] !== undefined) {
|
||||
// Theme has it's own rgb value
|
||||
continue;
|
||||
}
|
||||
const rgbValue = hexToRgb(value);
|
||||
if (rgbValue !== null) {
|
||||
try {
|
||||
const rgbValue = hex2rgb(value).join(",");
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
keys[prefixedRgbKey] = "";
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
PROCESSED_THEMES[themeName] = { styles, keys };
|
||||
PROCESSED_THEMES[cacheKey] = { styles, keys };
|
||||
return { styles, keys };
|
||||
};
|
||||
|
||||
|
@@ -22,9 +22,6 @@ const _load = (
|
||||
(element as HTMLScriptElement).async = true;
|
||||
if (type) {
|
||||
(element as HTMLScriptElement).type = type;
|
||||
// https://github.com/home-assistant/frontend/pull/6328
|
||||
(element as HTMLScriptElement).crossOrigin =
|
||||
url.substr(0, 1) === "/" ? "use-credentials" : "anonymous";
|
||||
}
|
||||
break;
|
||||
case "link":
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Map } from "leaflet";
|
||||
import type { Map, TileLayer } from "leaflet";
|
||||
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
export type LeafletModuleType = typeof import("leaflet");
|
||||
@@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode = false,
|
||||
darkMode?: boolean,
|
||||
draw = false
|
||||
): Promise<[Map, LeafletModuleType]> => {
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
@@ -28,15 +28,28 @@ export const setupLeafletMap = async (
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
createTileLayer(Leaflet, darkMode).addTo(map);
|
||||
|
||||
return [map, Leaflet];
|
||||
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
|
||||
|
||||
return [map, Leaflet, tileLayer];
|
||||
};
|
||||
|
||||
export const createTileLayer = (
|
||||
export const replaceTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
map: Map,
|
||||
tileLayer: TileLayer,
|
||||
darkMode: boolean
|
||||
): TileLayer => {
|
||||
map.removeLayer(tileLayer);
|
||||
tileLayer = createTileLayer(leaflet, darkMode);
|
||||
tileLayer.addTo(map);
|
||||
return tileLayer;
|
||||
};
|
||||
|
||||
const createTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
darkMode: boolean
|
||||
) => {
|
||||
): TileLayer => {
|
||||
return leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/${
|
||||
darkMode ? "dark_all" : "light_all"
|
||||
|
@@ -13,7 +13,7 @@ export const batteryIcon = (
|
||||
return "hass:battery-unknown";
|
||||
}
|
||||
|
||||
var icon = "hass:battery";
|
||||
let icon = "hass:battery";
|
||||
const batteryRound = Math.round(battery / 10) * 10;
|
||||
if (battery_charging && battery > 10) {
|
||||
icon += `-charging-${batteryRound}`;
|
||||
|
@@ -5,12 +5,16 @@ import { domainIcon } from "./domain_icon";
|
||||
import { batteryIcon } from "./battery_icon";
|
||||
|
||||
const fixedDeviceClassIcons = {
|
||||
current: "hass:current-ac",
|
||||
energy: "hass:flash",
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
temperature: "hass:thermometer",
|
||||
pressure: "hass:gauge",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
signal_strength: "hass:wifi",
|
||||
voltage: "hass:sine-wave",
|
||||
};
|
||||
|
||||
export const sensorIcon = (state: HassEntity) => {
|
||||
|
@@ -4,6 +4,6 @@ export const supportsFeature = (
|
||||
stateObj: HassEntity,
|
||||
feature: number
|
||||
): boolean => {
|
||||
// eslint-disable-next-line:no-bitwise
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (stateObj.attributes.supported_features! & feature) !== 0;
|
||||
};
|
||||
|
@@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
|
||||
|
||||
export const isValidEntityId = (entityId: string) =>
|
||||
validEntityId.test(entityId);
|
||||
|
||||
export const createValidEntityId = (input: string) =>
|
||||
input
|
||||
.toLowerCase()
|
||||
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
|
||||
.replace(/\W/g, "") // remove not allowed chars
|
||||
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
|
||||
.replace(/_$/, ""); // remove underscores at the end
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
RequestSelectedDetail,
|
||||
ListItem,
|
||||
RequestSelectedDetail,
|
||||
} from "@material/mwc-list/mwc-list-item";
|
||||
|
||||
export const shouldHandleRequestSelectedEvent = (
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): boolean => {
|
||||
if (!ev.detail.selected && ev.detail.source !== "property") {
|
||||
if (!ev.detail.selected || ev.detail.source !== "property") {
|
||||
return false;
|
||||
}
|
||||
(ev.target as ListItem).selected = false;
|
||||
(ev.currentTarget as ListItem).selected = false;
|
||||
return true;
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||
export const slugify = (value: string, delimiter = "-") => {
|
||||
export const slugify = (value: string, delimiter = "_") => {
|
||||
const a =
|
||||
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
|
||||
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
||||
|
@@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
callService() {
|
||||
this.progress = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
var el = this;
|
||||
var eventData = {
|
||||
const el = this;
|
||||
const eventData = {
|
||||
domain: this.domain,
|
||||
service: this.service,
|
||||
serviceData: this.serviceData,
|
||||
|
@@ -1,110 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "../ha-circular-progress";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
class HaProgressButton extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
outline: none;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
.success mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.error mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.progress {
|
||||
@apply --layout;
|
||||
@apply --layout-center-center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="container" id="container">
|
||||
<mwc-button
|
||||
id="button"
|
||||
disabled="[[computeDisabled(disabled, progress)]]"
|
||||
on-click="buttonTapped"
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
<template is="dom-if" if="[[progress]]">
|
||||
<div class="progress">
|
||||
<ha-circular-progress active size="small"></ha-circular-progress>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
progress: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
tempClass(className) {
|
||||
var classList = this.$.container.classList;
|
||||
classList.add(className);
|
||||
setTimeout(() => {
|
||||
classList.remove(className);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("click", (ev) => this.buttonTapped(ev));
|
||||
}
|
||||
|
||||
buttonTapped(ev) {
|
||||
if (this.progress) ev.stopPropagation();
|
||||
}
|
||||
|
||||
actionSuccess() {
|
||||
this.tempClass("success");
|
||||
}
|
||||
|
||||
actionError() {
|
||||
this.tempClass("error");
|
||||
}
|
||||
|
||||
computeDisabled(disabled, progress) {
|
||||
return disabled || progress;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-progress-button", HaProgressButton);
|
114
src/components/buttons/ha-progress-button.ts
Normal file
114
src/components/buttons/ha-progress-button.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import "@material/mwc-button";
|
||||
import type { Button } from "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
query,
|
||||
} from "lit-element";
|
||||
|
||||
import "../ha-circular-progress";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
class HaProgressButton extends LitElement {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
|
||||
@property({ type: Boolean }) public raised = false;
|
||||
|
||||
@query("mwc-button") private _button?: Button;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
@click=${this._buttonTapped}
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||
</div>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
public actionSuccess(): void {
|
||||
this._tempClass("success");
|
||||
}
|
||||
|
||||
public actionError(): void {
|
||||
this._tempClass("error");
|
||||
}
|
||||
|
||||
private _tempClass(className: string): void {
|
||||
this._button!.classList.add(className);
|
||||
setTimeout(() => {
|
||||
this._button!.classList.remove(className);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private _buttonTapped(ev: Event): void {
|
||||
if (this.progress) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
mwc-button.success {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].success {
|
||||
--mdc-theme-primary: var(--success-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
|
||||
mwc-button.error {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].error {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
|
||||
.progress {
|
||||
bottom: 0;
|
||||
margin-top: 4px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-progress-button": HaProgressButton;
|
||||
}
|
||||
}
|
@@ -3,19 +3,21 @@ import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
eventOptions,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { scroll } from "lit-virtualizer";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../common/search/search-input";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -24,8 +26,6 @@ import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-icon";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -70,6 +70,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableRowData {
|
||||
@@ -214,13 +215,15 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__table ${classMap({
|
||||
"auto-height": this.autoHeight,
|
||||
})}"
|
||||
role="table"
|
||||
aria-rowcount=${this._filteredData.length}
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${(this._filteredData.length || 1) * 53 + 57}px`
|
||||
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row">
|
||||
<div class="mdc-data-table__header-row" role="row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
@@ -240,8 +243,10 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
@@ -288,8 +293,8 @@ export class HaDataTable extends LitElement {
|
||||
${!this._filteredData.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row">
|
||||
<div class="mdc-data-table__cell grows center">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
${this.noDataText || "No data"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,12 +309,14 @@ export class HaDataTable extends LitElement {
|
||||
items: !this.hasFab
|
||||
? this._filteredData
|
||||
: [...this._filteredData, ...[{ empty: true }]],
|
||||
renderItem: (row: DataTableRowData) => {
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
if (row.empty) {
|
||||
return html` <div class="mdc-data-table__row"></div> `;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
aria-rowindex=${index}
|
||||
role="row"
|
||||
.rowId="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row ${classMap({
|
||||
@@ -328,6 +335,7 @@ export class HaDataTable extends LitElement {
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
role="cell"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@@ -341,40 +349,45 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${Object.entries(this.columns).map(
|
||||
([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
role="cell"
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
@@ -541,7 +554,7 @@ export class HaDataTable extends LitElement {
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-color: var(--divider-color);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
@@ -559,7 +572,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
@@ -578,7 +591,7 @@ export class HaDataTable extends LitElement {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -831,7 +844,7 @@ export class HaDataTable extends LitElement {
|
||||
right: 12px;
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 0 16px;
|
||||
}
|
||||
search-input {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
// To use comlink under ES5
|
||||
import "proxy-polyfill";
|
||||
import { expose } from "comlink";
|
||||
import "proxy-polyfill";
|
||||
import type {
|
||||
DataTableSortColumnData,
|
||||
DataTableRowData,
|
||||
SortingDirection,
|
||||
DataTableSortColumnData,
|
||||
SortableColumnContainer,
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const filterData = (
|
||||
@@ -19,7 +19,7 @@ const filterData = (
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
(column.filterKey ? row[key][column.filterKey] : row[key])
|
||||
String(column.filterKey ? row[key][column.filterKey] : row[key])
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
|
@@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
|
||||
}
|
||||
.daterangepicker td.in-range {
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--primary-text-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
}
|
||||
.daterangepicker td.active,
|
||||
.daterangepicker td.active:hover {
|
||||
|
@@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
|
||||
* @return {!Array<!HTMLElement>}
|
||||
*/
|
||||
getTabbableNodes: function (node) {
|
||||
var result = [];
|
||||
const result = [];
|
||||
// If there is at least one element with tabindex > 0, we need to sort
|
||||
// the final array by tabindex.
|
||||
var needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
||||
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
||||
if (needsSortByTabIndex) {
|
||||
return IronFocusablesHelper._sortByTabIndex(result);
|
||||
}
|
||||
@@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
var element = /** @type {!HTMLElement} */ (node);
|
||||
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
||||
var needsSort = tabIndex > 0;
|
||||
const element = /** @type {!HTMLElement} */ (node);
|
||||
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
||||
let needsSort = tabIndex > 0;
|
||||
if (tabIndex >= 0) {
|
||||
result.push(element);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
|
||||
// <input id="B" slot="b" tabindex="1">
|
||||
// </div>
|
||||
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
|
||||
var children;
|
||||
let children;
|
||||
if (element.localName === "content" || element.localName === "slot") {
|
||||
children = dom(element).getDistributedNodes();
|
||||
} else {
|
||||
@@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
|
||||
children = dom(element.shadowRoot || element.root || element).children;
|
||||
// /////////////////////////
|
||||
}
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
// Ensure method is always invoked to collect tabbable children.
|
||||
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
/* eslint-plugin-disable lit */
|
||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
||||
import "../ha-icon-button";
|
||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { formatTime } from "../../common/datetime/format_time";
|
||||
import "../ha-icon-button";
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
/* global Chart moment Color */
|
||||
@@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
return value;
|
||||
}
|
||||
const date = new Date(values[index].value);
|
||||
return formatTime(date);
|
||||
return formatTime(date, this.hass.language);
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
@@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
},
|
||||
};
|
||||
options = Chart.helpers.merge(options, this.data.options);
|
||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue;
|
||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
|
||||
if (this.data.type === "timeline") {
|
||||
this.set("isTimeline", true);
|
||||
if (this.data.colors !== undefined) {
|
||||
|
@@ -81,7 +81,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
? ""
|
||||
: this.image
|
||||
? this.image
|
||||
: state.attributes.entity_picture}"
|
||||
: state.attributes.entity_picture_local || state.attributes.entity_picture}"
|
||||
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
|
||||
.description="${this.name ? this.name : computeStateName(state)}"
|
||||
></ha-label-badge>
|
||||
|
@@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
|
||||
if (stateObj) {
|
||||
// hide icon if we have entity picture
|
||||
if (
|
||||
(stateObj.attributes.entity_picture && !this.overrideIcon) ||
|
||||
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
|
||||
this.overrideImage
|
||||
) {
|
||||
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
|
||||
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
67
src/components/ha-bar.ts
Normal file
67
src/components/ha-bar.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
svg,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import {
|
||||
getValueInPercentage,
|
||||
normalize,
|
||||
roundWithOneDecimal,
|
||||
} from "../util/calculate";
|
||||
|
||||
@customElement("ha-bar")
|
||||
export class HaBar extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
|
||||
@property({ type: Number }) public max = 100;
|
||||
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valuePrecentage = roundWithOneDecimal(
|
||||
getValueInPercentage(
|
||||
normalize(this.value, this.min, this.max),
|
||||
this.min,
|
||||
this.max
|
||||
)
|
||||
);
|
||||
|
||||
return svg`
|
||||
<svg>
|
||||
<g>
|
||||
<rect></rect>
|
||||
<rect width="${valuePrecentage}%"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
rect:first-child {
|
||||
width: 100%;
|
||||
fill: var(--ha-bar-background-color, var(--secondary-background-color));
|
||||
}
|
||||
rect:last-child {
|
||||
fill: var(--ha-bar-primary-color, var(--primary-color));
|
||||
rx: var(--ha-bar-border-radius, 4px);
|
||||
}
|
||||
svg {
|
||||
border-radius: var(--ha-bar-border-radius, 4px);
|
||||
height: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bar": HaBar;
|
||||
}
|
||||
}
|
@@ -1,21 +1,20 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
TemplateResult,
|
||||
property,
|
||||
LitElement,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import "./ha-icon-button";
|
||||
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@property() public buttons!: ToggleButton[];
|
||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@@ -23,21 +22,23 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map(
|
||||
(button) => html` <ha-icon-button
|
||||
.label=${button.label}
|
||||
.icon=${button.icon}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev): void {
|
||||
this.active = ev.target.value;
|
||||
this.active = ev.currentTarget.value;
|
||||
fireEvent(this, "value-changed", { value: this.active });
|
||||
}
|
||||
|
||||
@@ -48,12 +49,13 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
ha-icon-button {
|
||||
mwc-icon-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon-button::before {
|
||||
mwc-icon-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -65,22 +67,26 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
ha-icon-button[active]::before {
|
||||
mwc-icon-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
ha-icon-button:first-child {
|
||||
mwc-icon-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
ha-icon-button:last-child {
|
||||
mwc-icon-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-toggle-button": HaButtonToggleGroup;
|
||||
"ha-button-toggle-group": HaButtonToggleGroup;
|
||||
}
|
||||
}
|
||||
|
@@ -3,9 +3,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
@@ -18,37 +18,24 @@ import {
|
||||
fetchStreamUrl,
|
||||
} from "../data/camera";
|
||||
import { CameraEntity, HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
import "./ha-hls-player";
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: CameraEntity;
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@property({ type: Boolean }) public showControls = false;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
// We keep track if we should force MJPEG with a string
|
||||
// that way it automatically resets if we change entity.
|
||||
@internalProperty() private _forceMJPEG: string | undefined = undefined;
|
||||
@internalProperty() private _forceMJPEG?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
@internalProperty() private _url?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj || !this._attached) {
|
||||
if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -66,50 +53,22 @@ class HaCameraStream extends LitElement {
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<video
|
||||
<ha-hls-player
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
?controls=${this.showControls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
.hass=${this.hass}
|
||||
.url=${this._url!}
|
||||
></ha-hls-player>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const stateObjChanged = changedProps.has("stateObj");
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
|
||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
||||
|
||||
if (
|
||||
(!stateObjChanged && !attachedChanged) ||
|
||||
(stateObjChanged && oldEntityId === curEntityId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill.
|
||||
if (attachedChanged && !this._attached) {
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing to do if we are render MJPEG.
|
||||
if (this._shouldRenderMJPEG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
|
||||
if (curEntityId) {
|
||||
this._startHls();
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._forceMJPEG = undefined;
|
||||
this._getStreamUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,96 +80,35 @@ class HaCameraStream extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _videoEl(): HTMLVideoElement {
|
||||
return this.shadowRoot!.querySelector("video")!;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
// eslint-disable-next-line
|
||||
const Hls = ((await import(
|
||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
||||
)) as any).default as HLSModule;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
const videoEl = this._videoEl;
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
return;
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, Hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
return;
|
||||
this._url = url;
|
||||
} catch (err) {
|
||||
// Fails if we were unable to get a stream
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
// eslint-disable-next-line
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill(): void {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
@@ -66,7 +66,7 @@ export class HaCard extends LitElement {
|
||||
}
|
||||
|
||||
:host ::slotted(.card-actions) {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
border-top: 1px solid var(--divider-color, #e8e8e8);
|
||||
padding: 5px 16px;
|
||||
}
|
||||
`;
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
svg,
|
||||
html,
|
||||
customElement,
|
||||
unsafeCSS,
|
||||
SVGTemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
// @ts-ignore
|
||||
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
svg,
|
||||
SVGTemplateResult,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("ha-circular-progress")
|
||||
@@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement {
|
||||
@property()
|
||||
public size: "small" | "medium" | "large" = "medium";
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
protected render(): TemplateResult {
|
||||
let indeterminatePart: SVGTemplateResult;
|
||||
|
||||
if (this.size === "small") {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Editor } from "codemirror";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
property,
|
||||
PropertyValues,
|
||||
UpdatingElement,
|
||||
} from "lit-element";
|
||||
@@ -101,11 +101,6 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
.CodeMirror-scroll {
|
||||
max-height: var(--code-mirror-max-height, --code-mirror-height);
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
:host(.error-state) .CodeMirror-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
}
|
||||
@@ -113,7 +108,7 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
||||
color: var(--paper-dialog-color, var(--secondary-text-color));
|
||||
}
|
||||
.rtl .CodeMirror-vscrollbar {
|
||||
right: auto;
|
||||
@@ -122,6 +117,100 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
.rtl-gutter {
|
||||
width: 20px;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
.cm-s-default.CodeMirror {
|
||||
background-color: var(--code-editor-background-color, var(--card-background-color));
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.cm-s-default .CodeMirror-cursor {
|
||||
border-left: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
|
||||
background: rgba(var(--rgb-primary-color), 0.2);
|
||||
}
|
||||
|
||||
.cm-s-default .CodeMirror-line::selection,
|
||||
.cm-s-default .CodeMirror-line>span::selection,
|
||||
.cm-s-default .CodeMirror-line>span>span::selection {
|
||||
background: rgba(var(--rgb-primary-color), 0.2);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-keyword {
|
||||
color: var(--codemirror-keyword, #6262FF);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-operator {
|
||||
color: var(--codemirror-operator, #cda869);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable-2 {
|
||||
color: var(--codemirror-variable-2, #690);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-builtin {
|
||||
color: var(--codemirror-builtin, #9B7536);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-atom {
|
||||
color: var(--codemirror-atom, #F90);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-number {
|
||||
color: var(--codemirror-number, #ca7841);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-def {
|
||||
color: var(--codemirror-def, #8DA6CE);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string {
|
||||
color: var(--codemirror-string, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-string-2 {
|
||||
color: var(--codemirror-string-2, #bd6b18);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-comment {
|
||||
color: var(--codemirror-comment, #777);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable {
|
||||
color: var(--codemirror-variable, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-tag {
|
||||
color: var(--codemirror-tag, #997643);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-meta {
|
||||
color: var(--codemirror-meta, #000);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-attribute {
|
||||
color: var(--codemirror-attribute, #d6bb6d);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-property {
|
||||
color: var(--codemirror-property, #905);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-qualifier {
|
||||
color: var(--codemirror-qualifier, #690);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-variable-3 {
|
||||
color: var(--codemirror-variable-3, #07a);
|
||||
}
|
||||
|
||||
.cm-s-default .cm-type {
|
||||
color: var(--codemirror-type, #07a);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
this.codemirror = codeMirror(shadowRoot, {
|
||||
|
@@ -188,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
// origin is wheel center
|
||||
// returns {x: X, y: Y} object
|
||||
convertToCanvasCoordinates(clientX, clientY) {
|
||||
var svgPoint = this.interactionLayer.createSVGPoint();
|
||||
const svgPoint = this.interactionLayer.createSVGPoint();
|
||||
svgPoint.x = clientX;
|
||||
svgPoint.y = clientY;
|
||||
var cc = svgPoint.matrixTransform(
|
||||
const cc = svgPoint.matrixTransform(
|
||||
this.interactionLayer.getScreenCTM().inverse()
|
||||
);
|
||||
return { x: cc.x, y: cc.y };
|
||||
@@ -225,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
// Touch events
|
||||
|
||||
onTouchStart(ev) {
|
||||
var touch = ev.changedTouches[0];
|
||||
const touch = ev.changedTouches[0];
|
||||
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
|
||||
// return if we're not on the wheel
|
||||
if (!this.isInWheel(cc.x, cc.y)) {
|
||||
@@ -275,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// Process user input to color
|
||||
processUserSelect(ev) {
|
||||
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||
var hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||
const hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||
this.onColorSelect(hs);
|
||||
}
|
||||
|
||||
@@ -319,11 +319,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// set marker position to the given color
|
||||
setMarkerOnColor(hs) {
|
||||
var dist = hs.s * this.radius;
|
||||
var theta = ((hs.h - 180) / 180) * Math.PI;
|
||||
var markerdX = -dist * Math.cos(theta);
|
||||
var markerdY = -dist * Math.sin(theta);
|
||||
var translateString = `translate(${markerdX},${markerdY})`;
|
||||
const dist = hs.s * this.radius;
|
||||
const theta = ((hs.h - 180) / 180) * Math.PI;
|
||||
const markerdX = -dist * Math.cos(theta);
|
||||
const markerdY = -dist * Math.sin(theta);
|
||||
const translateString = `translate(${markerdX},${markerdY})`;
|
||||
this.marker.setAttribute("transform", translateString);
|
||||
this.tooltip.setAttribute("transform", translateString);
|
||||
}
|
||||
@@ -358,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// get angle (degrees)
|
||||
getAngle(dX, dY) {
|
||||
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
||||
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
||||
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
||||
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
||||
return angle;
|
||||
}
|
||||
|
||||
@@ -378,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
*/
|
||||
|
||||
getColor(x, y) {
|
||||
var hue = this.getAngle(x, y); // degrees, clockwise from right
|
||||
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
||||
var sat = Math.min(relativeDistance, 1); // Distance from center
|
||||
const hue = this.getAngle(x, y); // degrees, clockwise from right
|
||||
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
||||
const sat = Math.min(relativeDistance, 1); // Distance from center
|
||||
return { h: hue, s: sat };
|
||||
}
|
||||
|
||||
@@ -402,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
if (this.saturationSegments === 1) {
|
||||
hs.s = 1;
|
||||
} else {
|
||||
var segmentSize = 1 / this.saturationSegments;
|
||||
var saturationStep = 1 / (this.saturationSegments - 1);
|
||||
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
||||
const segmentSize = 1 / this.saturationSegments;
|
||||
const saturationStep = 1 / (this.saturationSegments - 1);
|
||||
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
||||
hs.s = Math.min(calculatedSat, 1);
|
||||
}
|
||||
}
|
||||
@@ -477,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
hueSegments = hueSegments || 360; // reset 0 segments to 360
|
||||
const angleStep = 360 / hueSegments;
|
||||
const halfAngleStep = angleStep / 2; // center segments on color
|
||||
for (var angle = 0; angle <= 360; angle += angleStep) {
|
||||
var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
||||
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
||||
for (let angle = 0; angle <= 360; angle += angleStep) {
|
||||
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
||||
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
||||
context.beginPath();
|
||||
context.moveTo(cX, cY);
|
||||
context.arc(
|
||||
@@ -492,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
);
|
||||
context.closePath();
|
||||
// gradient
|
||||
var gradient = context.createRadialGradient(
|
||||
const gradient = context.createRadialGradient(
|
||||
cX,
|
||||
cY,
|
||||
0,
|
||||
@@ -507,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
if (saturationSegments > 0) {
|
||||
const ratioStep = 1 / saturationSegments;
|
||||
let ratio = 0;
|
||||
for (var stop = 1; stop < saturationSegments; stop += 1) {
|
||||
var prevLighness = lightness;
|
||||
for (let stop = 1; stop < saturationSegments; stop += 1) {
|
||||
const prevLighness = lightness;
|
||||
ratio = stop * ratioStep;
|
||||
lightness = 100 - 50 * ratio;
|
||||
gradient.addColorStop(
|
||||
|
@@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
||||
}
|
||||
|
||||
|
@@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyOpenTilt && !assumedState;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyClosedTilt && !assumedState;
|
||||
}
|
||||
|
||||
|
@@ -163,13 +163,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -179,12 +172,30 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
paper-input {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
max-width: 250px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
paper-input:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
paper-input {
|
||||
min-width: inherit;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import "@material/mwc-dialog";
|
||||
import type { Dialog } from "@material/mwc-dialog";
|
||||
import { style } from "@material/mwc-dialog/mwc-dialog-css";
|
||||
import "./ha-icon-button";
|
||||
import { css, CSSResult, customElement, html } from "lit-element";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, CSSResult, customElement, html } from "lit-element";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||
|
||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||
${title}
|
||||
<span class="header_title">${title}</span>
|
||||
<mwc-icon-button
|
||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||
dialogAction="close"
|
||||
@@ -23,6 +23,10 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends MwcDialog {
|
||||
public scrollToPos(x: number, y: number) {
|
||||
this.contentElement.scrollTo(x, y);
|
||||
}
|
||||
|
||||
protected renderHeading() {
|
||||
return html`<slot name="heading">
|
||||
${super.renderHeading()}
|
||||
@@ -34,10 +38,12 @@ export class HaDialog extends MwcDialog {
|
||||
style,
|
||||
css`
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertial-align-dialog, center);
|
||||
@@ -50,10 +56,20 @@ export class HaDialog extends MwcDialog {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, 20px 24px);
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: max(
|
||||
var(--dialog-content-padding, 20px),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header_button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@@ -61,10 +77,17 @@ export class HaDialog extends MwcDialog {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
}
|
||||
[dir="rtl"].header_button {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
[dir="rtl"].header_title {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -54,7 +54,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-paper-slider
|
||||
pin=""
|
||||
pin
|
||||
editable
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
@@ -111,6 +112,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
ha-paper-slider {
|
||||
width: 100%;
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("paper-dropdown-menu") private _input?: HTMLElement;
|
||||
@query("ha-paper-dropdown-menu") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-dropdown-menu .label=${this.label}>
|
||||
<ha-paper-dropdown-menu .label=${this.label}>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
@@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</ha-paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-dropdown-menu {
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
@@ -100,7 +100,7 @@ export interface HaFormTimeData {
|
||||
}
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema;
|
||||
schema: HaFormSchema | HaFormSchema[];
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: string;
|
||||
suffix?: string;
|
||||
@@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property() public data!: HaFormDataContainer | HaFormData;
|
||||
|
||||
@property() public schema!: HaFormSchema;
|
||||
@property() public schema!: HaFormSchema | HaFormSchema[];
|
||||
|
||||
@property() public error;
|
||||
|
||||
@@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeError(error, schema: HaFormSchema) {
|
||||
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema;
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
const data = this.data as HaFormDataContainer;
|
||||
data[schema.name] = ev.detail.value;
|
||||
fireEvent(this, "value-changed", {
|
||||
|
@@ -9,23 +9,17 @@ import {
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
return (percentage * 180) / 100;
|
||||
};
|
||||
|
||||
const normalize = (value: number, min: number, max: number) => {
|
||||
if (value > max) return max;
|
||||
if (value < min) return min;
|
||||
return value;
|
||||
};
|
||||
|
||||
const getValueInPercentage = (value: number, min: number, max: number) => {
|
||||
const newMax = max - min;
|
||||
const newVal = value - min;
|
||||
return (100 * newVal) / newMax;
|
||||
};
|
||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
@customElement("ha-gauge")
|
||||
export class Gauge extends LitElement {
|
||||
@@ -69,9 +63,28 @@ export class Gauge extends LitElement {
|
||||
></path>
|
||||
<path
|
||||
class="value"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||
></path>
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
)}
|
||||
>
|
||||
${
|
||||
isSafari
|
||||
? svg`<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="${this._angle} 50 50"
|
||||
dur="1s"
|
||||
/>`
|
||||
: ""
|
||||
}
|
||||
</path>
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
@@ -106,8 +119,8 @@ export class Gauge extends LitElement {
|
||||
fill: none;
|
||||
stroke-width: 15;
|
||||
stroke: var(--gauge-color);
|
||||
transition: all 1000ms ease 0s;
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.gauge {
|
||||
display: block;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
|
||||
// @ts-ignore
|
||||
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
||||
import { css, customElement, html, LitElement, unsafeCSS } from "lit-element";
|
||||
|
||||
@customElement("ha-header-bar")
|
||||
export class HaHeaderBar extends LitElement {
|
||||
|
216
src/components/ha-hls-player.ts
Normal file
216
src/components/ha-hls-player.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "muted" })
|
||||
public muted = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autoplay" })
|
||||
public autoPlay = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "playsinline" })
|
||||
public playsInline = false;
|
||||
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._attached) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
?muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
const urlChanged = changedProps.has("url");
|
||||
|
||||
if (!urlChanged && !attachedChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill
|
||||
if (attachedChanged && !this._attached) {
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
this._destroyPolyfill();
|
||||
this._startHls();
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
let hls: any;
|
||||
const videoEl = this._videoEl;
|
||||
this._useExoPlayer = await this._getUseExoPlayer();
|
||||
if (!this._useExoPlayer) {
|
||||
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._videoEl.innerHTML = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = this.url;
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSExoPlayer(url: string) {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.sendMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: new URL(url, window.location.href).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
if (this._useExoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
video {
|
||||
display: block;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-hls-player": HaHLSPlayer;
|
||||
}
|
||||
}
|
@@ -106,6 +106,7 @@ const mdiRenameMapping = {
|
||||
pot: "pot-steam",
|
||||
ruby: "language-ruby",
|
||||
sailing: "sail-boat",
|
||||
scooter: "human-scooter",
|
||||
settings: "cog",
|
||||
"settings-box": "cog-box",
|
||||
"settings-outline": "cog-outline",
|
||||
@@ -193,6 +194,7 @@ const mdiRemovedIcons = new Set([
|
||||
"medium",
|
||||
"meetup",
|
||||
"mixcloud",
|
||||
"mixer",
|
||||
"nfc-off",
|
||||
"npm-variant",
|
||||
"npm-variant-outline",
|
||||
|
@@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
|
||||
{
|
||||
breaks: this.breaks,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
},
|
||||
{
|
||||
allowSvg: this.allowSvg,
|
||||
|
@@ -57,6 +57,10 @@ class HaMarkdown extends LitElement {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: 3px;
|
||||
}
|
||||
ha-markdown-element svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
}
|
||||
ha-markdown-element code {
|
||||
font-size: 85%;
|
||||
padding: 0.2em 0.4em;
|
||||
@@ -70,8 +74,8 @@ class HaMarkdown extends LitElement {
|
||||
line-height: 1.45;
|
||||
}
|
||||
ha-markdown-element h2 {
|
||||
font-size: 1.5em !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -68,6 +68,10 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
`;
|
||||
tpl.content.appendChild(styleEl);
|
||||
return tpl;
|
||||
|
226
src/components/ha-picture-upload.ts
Normal file
226
src/components/ha-picture-upload.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiImagePlus } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-svg-icon";
|
||||
import {
|
||||
showImageCropperDialog,
|
||||
CropOptions,
|
||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
|
||||
@customElement("ha-picture-upload")
|
||||
export class HaPictureUpload extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property() public value: string | null = null;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public crop = false;
|
||||
|
||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||
|
||||
@property({ type: Number }) public size = 512;
|
||||
|
||||
@internalProperty() private _error = "";
|
||||
|
||||
@internalProperty() private _uploading = false;
|
||||
|
||||
@internalProperty() private _drag = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("_drag")) {
|
||||
(this.shadowRoot!.querySelector(
|
||||
"paper-input-container"
|
||||
) as any)._setFocused(this._drag);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this._uploading
|
||||
? html`<ha-circular-progress
|
||||
alt="Uploading"
|
||||
size="large"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<label for="input">
|
||||
<paper-input-container
|
||||
.alwaysFloatLabel=${Boolean(this.value)}
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
class=${classMap({
|
||||
dragged: this._drag,
|
||||
})}
|
||||
>
|
||||
<label for="input" slot="label">
|
||||
${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
</label>
|
||||
<iron-input slot="input">
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
@change=${this._handleFilePicked}
|
||||
/>
|
||||
${this.value ? html`<img .src=${this.value} />` : ""}
|
||||
</iron-input>
|
||||
${this.value
|
||||
? html`<mwc-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearPicture}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-icon-button slot="suffix">
|
||||
<ha-svg-icon .path=${mdiImagePlus}></ha-svg-icon>
|
||||
</mwc-icon-button>`}
|
||||
</paper-input-container>
|
||||
</label>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.dataTransfer?.files) {
|
||||
if (this.crop) {
|
||||
this._cropFile(ev.dataTransfer.files[0]);
|
||||
} else {
|
||||
this._uploadFile(ev.dataTransfer.files[0]);
|
||||
}
|
||||
}
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private _handleDragStart(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = true;
|
||||
}
|
||||
|
||||
private _handleDragEnd(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private async _handleFilePicked(ev) {
|
||||
if (this.crop) {
|
||||
this._cropFile(ev.target.files[0]);
|
||||
} else {
|
||||
this._uploadFile(ev.target.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private async _cropFile(file: File) {
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.picture-upload.unsupported_format"
|
||||
);
|
||||
return;
|
||||
}
|
||||
showImageCropperDialog(this, {
|
||||
file,
|
||||
options: this.cropOptions || {
|
||||
round: false,
|
||||
aspectRatio: NaN,
|
||||
},
|
||||
croppedCallback: (croppedFile) => {
|
||||
this._uploadFile(croppedFile);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _uploadFile(file: File) {
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.picture-upload.unsupported_format"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const media = await createImage(this.hass, file);
|
||||
this.value = generateImageThumbnailUrl(media.id, this.size);
|
||||
fireEvent(this, "change");
|
||||
} catch (err) {
|
||||
this._error = err.toString();
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearPicture(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = null;
|
||||
this._error = "";
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
paper-input-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
paper-input-container.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
img {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picture-upload": HaPictureUpload;
|
||||
}
|
||||
}
|
20
src/components/ha-radio.ts
Normal file
20
src/components/ha-radio.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import "@material/mwc-radio";
|
||||
import type { Radio } from "@material/mwc-radio";
|
||||
import { customElement } from "lit-element";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
const MwcRadio = customElements.get("mwc-radio") as Constructor<Radio>;
|
||||
|
||||
@customElement("ha-radio")
|
||||
export class HaRadio extends MwcRadio {
|
||||
public firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio": HaRadio;
|
||||
}
|
||||
}
|
@@ -97,6 +97,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
</h3>
|
||||
<a
|
||||
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${this.hass.localize(`component.${entry.domain}.title`)}:
|
||||
${entry.title}
|
||||
@@ -116,7 +117,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.device")}:
|
||||
</h3>
|
||||
<a href="/config/devices/device/${relatedDeviceId}">
|
||||
<a
|
||||
href="/config/devices/device/${relatedDeviceId}"
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${device.name_by_user || device.name}
|
||||
</a>
|
||||
`;
|
||||
@@ -134,7 +138,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.area")}:
|
||||
</h3>
|
||||
<a href="/config/areas/area/${relatedAreaId}">
|
||||
<a
|
||||
href="/config/areas/area/${relatedAreaId}"
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${area.name}
|
||||
</a>
|
||||
`;
|
||||
@@ -278,6 +285,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _navigateAwayClose() {
|
||||
// allow new page to open before closing dialog
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, this.itemType, this.itemId);
|
||||
await this.updateComplete;
|
||||
|
@@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
|
||||
}
|
||||
|
||||
_getDescription(hass, domain, service) {
|
||||
var domainServices = hass.services[domain];
|
||||
const domainServices = hass.services[domain];
|
||||
if (!domainServices) return "";
|
||||
var serviceObject = domainServices[service];
|
||||
const serviceObject = domainServices[service];
|
||||
if (!serviceObject) return "";
|
||||
return serviceObject.description;
|
||||
}
|
||||
|
59
src/components/ha-settings-row.ts
Normal file
59
src/components/ha-settings-row.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
SVGTemplateResult,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
@customElement("ha-settings-row")
|
||||
export class HaSettingsRow extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "three-line" })
|
||||
public threeLine = false;
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
paper-item-body {
|
||||
padding-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
align-content: normal;
|
||||
align-self: auto;
|
||||
align-items: center;
|
||||
}
|
||||
:host([narrow]) {
|
||||
align-items: normal;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-settings-row": HaSettingsRow;
|
||||
}
|
||||
}
|
77
src/components/ha-sidebar-sort-styles.ts
Normal file
77
src/components/ha-sidebar-sort-styles.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { html } from "lit-element";
|
||||
|
||||
export const sortStyles = html`
|
||||
<style>
|
||||
#sortable a:nth-of-type(2n) paper-icon-item {
|
||||
animation-name: keyframes1;
|
||||
animation-iteration-count: infinite;
|
||||
transform-origin: 50% 10%;
|
||||
animation-delay: -0.75s;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
||||
animation-name: keyframes2;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
transform-origin: 30% 5%;
|
||||
animation-delay: -0.5s;
|
||||
animation-duration: 0.33s;
|
||||
}
|
||||
|
||||
#sortable {
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes keyframes1 {
|
||||
0% {
|
||||
transform: rotate(-1deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(1.5deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes keyframes2 {
|
||||
0% {
|
||||
transform: rotate(1deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(-1.5deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
:host([expanded]) .hide-panel {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
paper-icon-item.hidden-panel,
|
||||
paper-icon-item.hidden-panel span,
|
||||
paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
@@ -1,9 +1,12 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCellphoneCog,
|
||||
mdiMenuOpen,
|
||||
mdiClose,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlus,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
@@ -13,20 +16,24 @@ import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
customElement,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { guard } from "lit-html/directives/guard";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../common/decorators/local-storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
import { ActionHandlerDetail } from "../data/lovelace";
|
||||
import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
@@ -35,6 +42,8 @@ import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-menu-button";
|
||||
@@ -54,11 +63,39 @@ const SORT_VALUE_URL_PATHS = {
|
||||
config: 11,
|
||||
};
|
||||
|
||||
const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
const panelSorter = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
a: PanelInfo,
|
||||
b: PanelInfo
|
||||
) => {
|
||||
const indexA = reverseSort.indexOf(a.url_path);
|
||||
const indexB = reverseSort.indexOf(b.url_path);
|
||||
if (indexA !== indexB) {
|
||||
if (indexA < indexB) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return defaultPanelSorter(defaultPanel, a, b);
|
||||
};
|
||||
|
||||
const defaultPanelSorter = (
|
||||
defaultPanel: string,
|
||||
a: PanelInfo,
|
||||
b: PanelInfo
|
||||
) => {
|
||||
// Put all the Lovelace at the top.
|
||||
const aLovelace = a.component_name === "lovelace";
|
||||
const bLovelace = b.component_name === "lovelace";
|
||||
|
||||
if (a.url_path === defaultPanel) {
|
||||
return -1;
|
||||
}
|
||||
if (b.url_path === defaultPanel) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aLovelace && bLovelace) {
|
||||
return compare(a.title!, b.title!);
|
||||
}
|
||||
@@ -85,30 +122,45 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
return compare(a.title!, b.title!);
|
||||
};
|
||||
|
||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const panels = hass.panels;
|
||||
if (!panels) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title || panel.url_path === hass.defaultPanel) {
|
||||
return;
|
||||
const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[]
|
||||
): [PanelInfo[], PanelInfo[]] => {
|
||||
if (!panels) {
|
||||
return [[], []];
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
});
|
||||
|
||||
beforeSpacer.sort(panelSorter);
|
||||
afterSpacer.sort(panelSorter);
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
return [beforeSpacer, afterSpacer];
|
||||
};
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
});
|
||||
|
||||
const reverseSort = [...panelsOrder].reverse();
|
||||
|
||||
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||
|
||||
return [beforeSpacer, afterSpacer];
|
||||
}
|
||||
);
|
||||
|
||||
let Sortable;
|
||||
|
||||
let sortStyles: TemplateResult;
|
||||
|
||||
@customElement("ha-sidebar")
|
||||
class HaSidebar extends LitElement {
|
||||
@@ -124,9 +176,13 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@internalProperty() private _notifications?: PersistentNotification[];
|
||||
|
||||
@internalProperty() private _editMode = false;
|
||||
|
||||
// property used only in css
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean, reflect: true }) private _rtl = false;
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@internalProperty() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
|
||||
@@ -134,6 +190,20 @@ class HaSidebar extends LitElement {
|
||||
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarPanelOrder", true, {
|
||||
attribute: false,
|
||||
})
|
||||
private _panelOrder: string[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarHiddenPanels", true, {
|
||||
attribute: false,
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
private _sortable?;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
||||
@@ -141,7 +211,12 @@ class HaSidebar extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(hass);
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
hass.panels,
|
||||
hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
@@ -152,9 +227,8 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanel(hass);
|
||||
|
||||
return html`
|
||||
${this._editMode ? sortStyles : ""}
|
||||
<div class="menu">
|
||||
${!this.narrow
|
||||
? html`
|
||||
@@ -170,40 +244,69 @@ class HaSidebar extends LitElement {
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<span class="title">Home Assistant</span>
|
||||
<div class="title">
|
||||
${this._editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
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}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this._editMode,
|
||||
disabled: this._editMode,
|
||||
})}
|
||||
>
|
||||
${this._renderPanel(
|
||||
defaultPanel.url_path,
|
||||
defaultPanel.title || hass.localize("panel.states"),
|
||||
defaultPanel.icon,
|
||||
!defaultPanel.icon ? mdiViewDashboard : undefined
|
||||
)}
|
||||
${beforeSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
undefined
|
||||
)
|
||||
)}
|
||||
${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>
|
||||
|
||||
${afterSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
undefined
|
||||
)
|
||||
)}
|
||||
${this._editMode && this._hiddenPanels.length
|
||||
? html`
|
||||
${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === "lovelace"
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === "lovelace"
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="hide-panel"
|
||||
.panel=${url}
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
<div class="spacer" disabled></div>
|
||||
`
|
||||
: ""}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
@@ -277,7 +380,11 @@ class HaSidebar extends LitElement {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${hass.user}
|
||||
.hass=${hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${hass.user ? hass.user.name : ""}
|
||||
@@ -295,7 +402,11 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications")
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("_editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
changedProps.has("_hiddenPanels") ||
|
||||
(changedProps.has("_panelOrder") && !this._editMode)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -312,6 +423,7 @@ class HaSidebar extends LitElement {
|
||||
hass.panelUrl !== oldHass.panelUrl ||
|
||||
hass.user !== oldHass.user ||
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.language !== oldHass.language ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
);
|
||||
@@ -339,12 +451,14 @@ class HaSidebar extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rtl = computeRTL(this.hass);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
|
||||
if (selectedEl) {
|
||||
@@ -358,6 +472,74 @@ class HaSidebar extends LitElement {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||
if (ev.detail.action !== "hold") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Sortable) {
|
||||
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||
import("sortablejs/modular/sortable.core.esm"),
|
||||
import("./ha-sidebar-sort-styles"),
|
||||
]);
|
||||
|
||||
sortStyles = sortStylesImport.sortStyles;
|
||||
|
||||
Sortable = sortableImport.Sortable;
|
||||
Sortable.mount(sortableImport.OnSpill);
|
||||
Sortable.mount(sortableImport.AutoScroll());
|
||||
}
|
||||
this._editMode = true;
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
this._createSortable();
|
||||
}
|
||||
|
||||
private _createSortable() {
|
||||
this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), {
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
dataIdAttr: "data-panel",
|
||||
onSort: async () => {
|
||||
this._panelOrder = this._sortable.toArray();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _closeEditMode() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
this._editMode = false;
|
||||
}
|
||||
|
||||
private async _hidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const panel = (ev.target as any).panel;
|
||||
if (this._hiddenPanels.includes(panel)) {
|
||||
return;
|
||||
}
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||
this._renderEmptySortable = true;
|
||||
await this.updateComplete;
|
||||
this._renderEmptySortable = false;
|
||||
}
|
||||
|
||||
private async _unhidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const index = this._hiddenPanels.indexOf((ev.target as any).panel);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
this._hiddenPanels.splice(index, 1);
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels];
|
||||
this._renderEmptySortable = true;
|
||||
await this.updateComplete;
|
||||
this._renderEmptySortable = false;
|
||||
}
|
||||
|
||||
private _itemMouseEnter(ev: MouseEvent) {
|
||||
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||
@@ -454,6 +636,19 @@ class HaSidebar extends LitElement {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
private _renderPanels(panels: PanelInfo[]) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === "lovelace"
|
||||
? this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.url_path === "lovelace" ? undefined : panel.icon,
|
||||
panel.url_path === "lovelace" ? mdiViewDashboard : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
@@ -477,269 +672,296 @@ class HaSidebar extends LitElement {
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
${this._editMode
|
||||
? html`<ha-svg-icon
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
.path=${mdiClose}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: 256px;
|
||||
}
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) {
|
||||
border-right: 0;
|
||||
border-left: 1px solid var(--divider-color);
|
||||
}
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) .menu {
|
||||
padding-left: 8.5px;
|
||||
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-right));
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: 256px;
|
||||
}
|
||||
.title {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
paper-listbox {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px - env(safe-area-inset-bottom));
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: none;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
:host([rtl]) paper-listbox {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
paper-listbox::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
paper-listbox::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px 8px;
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
}
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
}
|
||||
:host([rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
background: none;
|
||||
}
|
||||
ha-icon[slot="item-icon"],
|
||||
ha-svg-icon[slot="item-icon"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px 8px;
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
}
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
}
|
||||
:host([_rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
ha-icon[slot="item-icon"],
|
||||
ha-svg-icon[slot="item-icon"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
paper-icon-item span {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
a.iron-selected paper-icon-item ha-icon,
|
||||
a.iron-selected paper-icon-item ha-svg-icon {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
a.iron-selected .item-text {
|
||||
color: var(--sidebar-selected-text-color);
|
||||
}
|
||||
|
||||
paper-icon-item span {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.iron-selected paper-icon-item ha-icon,
|
||||
a.iron-selected paper-icon-item ha-svg-icon {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
.divider {
|
||||
bottom: 112px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .profile {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
}
|
||||
:host([rtl]) .profile paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
:host([rtl]) .profile .item-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
a.iron-selected .item-text {
|
||||
color: var(--sidebar-selected-text-color);
|
||||
}
|
||||
.notification-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
background-color: var(--accent-color);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.divider {
|
||||
bottom: 112px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
display: flex;
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
}
|
||||
:host([_rtl]) .profile paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
:host([_rtl]) .profile .item-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.subheader {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
background-color: var(--accent-color);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:host([_rtl]) .menu mwc-icon-button {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`;
|
||||
:host([rtl]) .menu mwc-icon-button {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -50,6 +50,8 @@ export class HaYamlEditor extends LitElement {
|
||||
try {
|
||||
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
alert(`There was an error converting to YAML: ${err}`);
|
||||
}
|
||||
afterNextRender(() => {
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
LeafletMouseEvent,
|
||||
Map,
|
||||
Marker,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -20,21 +21,27 @@ import {
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-location-editor")
|
||||
class LocationEditor extends LitElement {
|
||||
@property() public location?: [number, number];
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public radius?: number;
|
||||
@property({ type: Array }) public location?: [number, number];
|
||||
|
||||
@property({ type: Number }) public radius?: number;
|
||||
|
||||
@property() public radiusColor?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property({ type: Boolean }) public darkMode?: boolean;
|
||||
|
||||
public fitZoom = 16;
|
||||
|
||||
private _iconEl?: DivIcon;
|
||||
@@ -46,6 +53,8 @@ class LocationEditor extends LitElement {
|
||||
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _locationMarker?: Marker | Circle;
|
||||
|
||||
public fitMap(): void {
|
||||
@@ -97,6 +106,22 @@ class LocationEditor extends LitElement {
|
||||
if (changedProps.has("icon")) {
|
||||
this._updateIcon();
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes?.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@@ -104,9 +129,9 @@ class LocationEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
this.darkMode ?? this.hass.themes?.darkMode,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
@@ -255,9 +280,6 @@ class LocationEditor extends LitElement {
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.leaflet-edit-move {
|
||||
border-radius: 50%;
|
||||
cursor: move !important;
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
Map,
|
||||
Marker,
|
||||
MarkerOptions,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -21,8 +22,10 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -47,6 +50,8 @@ export interface MarkerLocation {
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public locations?: MarkerLocation[];
|
||||
|
||||
public fitZoom = 16;
|
||||
@@ -57,6 +62,8 @@ export class HaLocationsEditor extends LitElement {
|
||||
// eslint-disable-next-line
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _locationMarkers?: { [key: string]: Marker | Circle };
|
||||
|
||||
private _circles: { [key: string]: Circle } = {};
|
||||
@@ -116,6 +123,22 @@ export class HaLocationsEditor extends LitElement {
|
||||
if (changedProps.has("locations")) {
|
||||
this._updateMarkers();
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@@ -123,9 +146,9 @@ export class HaLocationsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
this.hass.themes.darkMode,
|
||||
true
|
||||
);
|
||||
this._updateMarkers();
|
||||
@@ -290,9 +313,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "../ha-icon-button";
|
||||
import { Circle, Layer, Map, Marker } from "leaflet";
|
||||
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
@@ -22,11 +23,11 @@ import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-map")
|
||||
class HaMap extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entities?: string[];
|
||||
|
||||
@property() public darkMode = false;
|
||||
@property() public darkMode?: boolean;
|
||||
|
||||
@property() public zoom?: number;
|
||||
|
||||
@@ -35,6 +36,8 @@ class HaMap extends LitElement {
|
||||
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
@@ -122,6 +125,20 @@ class HaMap extends LitElement {
|
||||
if (changedProps.has("hass")) {
|
||||
this._drawEntities();
|
||||
this._fitMap();
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +147,9 @@ class HaMap extends LitElement {
|
||||
}
|
||||
|
||||
private async loadMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
this.darkMode
|
||||
this.darkMode ?? this.hass.themes.darkMode
|
||||
);
|
||||
this._drawEntities();
|
||||
this._leafletMap.invalidateSize();
|
||||
@@ -229,7 +246,8 @@ class HaMap extends LitElement {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: this.darkMode ? "dark" : "light",
|
||||
className:
|
||||
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
|
||||
}),
|
||||
interactive: false,
|
||||
title,
|
||||
|
110
src/components/media-player/dialog-media-player-browse.ts
Normal file
110
src/components/media-player/dialog-media-player-browse.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "./ha-media-player-browse";
|
||||
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
|
||||
@customElement("dialog-media-player-browse")
|
||||
class DialogMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _entityId!: string;
|
||||
|
||||
@internalProperty() private _mediaContentId?: string;
|
||||
|
||||
@internalProperty() private _mediaContentType?: string;
|
||||
|
||||
@internalProperty() private _action?: MediaPlayerBrowseAction;
|
||||
|
||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
this._params = params;
|
||||
this._entityId = this._params.entityId;
|
||||
this._mediaContentId = this._params.mediaContentId;
|
||||
this._mediaContentType = this._params.mediaContentType;
|
||||
this._action = this._params.action || "play";
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", {dialog: this.localName});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-media-player-browse
|
||||
dialog
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
this._params!.mediaPickedCallback(ev.detail);
|
||||
if (this._action !== "play") {
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 8;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
}
|
||||
ha-media-player-browse {
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-media-player-browse": DialogMediaPlayerBrowse;
|
||||
}
|
||||
}
|
770
src/components/media-player/ha-media-player-browse.ts
Normal file
770
src/components/media-player/ha-media-player-browse.ts
Normal file
@@ -0,0 +1,770 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab/mwc-fab";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
browseMediaPlayer,
|
||||
BROWSER_SOURCE,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-picked": MediaPickedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property() public mediaContentId?: string;
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: MediaPlayerBrowseAction = "play";
|
||||
|
||||
@property({ type: Boolean }) public dialog = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
||||
private _narrow = false;
|
||||
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public navigateBack() {
|
||||
this._mediaPlayerItems!.pop();
|
||||
const item = this._mediaPlayerItems!.pop();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this._navigate(item);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentItem = this._mediaPlayerItems[
|
||||
this._mediaPlayerItems.length - 1
|
||||
];
|
||||
|
||||
const previousItem: MediaPlayerItem | undefined =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
|
||||
const hasExpandableChildren:
|
||||
| MediaPlayerItem
|
||||
| undefined = this._hasExpandableChildren(currentItem.children);
|
||||
|
||||
const showImages: boolean | undefined = currentItem.children?.some(
|
||||
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
|
||||
);
|
||||
|
||||
const mediaType = this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${currentItem.media_content_type}`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="header ${classMap({
|
||||
"no-img": !currentItem.thumbnail,
|
||||
})}"
|
||||
>
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: url(${currentItem.thumbnail})"
|
||||
>
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<mwc-fab
|
||||
mini
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
? html`
|
||||
<div class="previous-title" @click=${this.navigateBack}>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${mediaType
|
||||
? html`
|
||||
<h2 class="subtitle">
|
||||
${mediaType}
|
||||
</h2>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
${this.dialog
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
aria-label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
@click=${this._closeDialogAction}
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.children?.length
|
||||
? hasExpandableChildren
|
||||
? html`
|
||||
<div class="children">
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigateForward}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._actionClicked}
|
||||
.item=${child}
|
||||
graphic="avatar"
|
||||
hasMeta
|
||||
>
|
||||
<div
|
||||
class="graphic"
|
||||
style=${ifDefined(
|
||||
showImages && child.thumbnail
|
||||
? `background-image: url(${child.thumbnail})`
|
||||
: undefined
|
||||
)}
|
||||
slot="graphic"
|
||||
>
|
||||
<mwc-icon-button
|
||||
class="play ${classMap({
|
||||
show: !showImages || !child.thumbnail,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
<span>${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: this.hass.localize("ui.components.media-browser.no_items")}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachObserver();
|
||||
|
||||
this.addEventListener("scroll", this._scroll, { passive: true });
|
||||
this.addEventListener("touchmove", this._scroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
!changedProps.has("entityId") &&
|
||||
!changedProps.has("mediaContentId") &&
|
||||
!changedProps.has("mediaContentType") &&
|
||||
!changedProps.has("action")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchData(this.mediaContentId, this.mediaContentType).then(
|
||||
(itemData) => {
|
||||
this._mediaPlayerItems = [itemData];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _actionClicked(ev: MouseEvent): void {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
|
||||
this._runAction(item);
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", { item });
|
||||
}
|
||||
|
||||
private async _navigateForward(ev: MouseEvent): Promise<void> {
|
||||
const target = ev.currentTarget as any;
|
||||
const item: MediaPlayerItem = target.item;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this._navigate(item);
|
||||
}
|
||||
|
||||
private async _navigate(item: MediaPlayerItem) {
|
||||
const itemData = await this._fetchData(
|
||||
item.media_content_id,
|
||||
item.media_content_type
|
||||
);
|
||||
|
||||
this.scrollTo(0, 0);
|
||||
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
|
||||
}
|
||||
|
||||
private async _fetchData(
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
const itemData =
|
||||
this.entityId !== BROWSER_SOURCE
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
return itemData;
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
|
||||
}
|
||||
|
||||
private _scroll(): void {
|
||||
if (this.scrollTop > (this._narrow ? 224 : 125)) {
|
||||
this.setAttribute("scroll", "");
|
||||
} else if (this.scrollTop === 0) {
|
||||
this.removeAttribute("scroll");
|
||||
}
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
private _hasExpandableChildren = memoizeOne((children) =>
|
||||
children.find((item: MediaPlayerItem) => item.can_expand)
|
||||
);
|
||||
|
||||
private _closeDialogAction(): void {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
padding: 0px 0px 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.header_button {
|
||||
position: relative;
|
||||
top: 14px;
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--card-background-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
padding: 20px 24px 10px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-content .img {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s, height 0.4s;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-info mwc-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.breadcrumb .title {
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb .previous-title {
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb .subtitle {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 0;
|
||||
transition: height 0.5s, margin 0.5s;
|
||||
}
|
||||
|
||||
/* ============= CHILDREN ============= */
|
||||
|
||||
mwc-list {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
mwc-list li:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
mwc-list li[divider] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(var(--media-browse-item-size, 175px), 0.33fr)
|
||||
);
|
||||
grid-gap: 16px;
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
:host(:not([narrow])) .children {
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
.child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ha-card-parent {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.child .folder,
|
||||
.child .play {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.child .folder {
|
||||
color: var(--secondary-text-color);
|
||||
top: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
left: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .play {
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.child .play:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.child .type {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
mwc-list-item .graphic {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
mwc-list-item .graphic .play {
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: 50%;
|
||||
--mdc-icon-button-size: 40px;
|
||||
}
|
||||
|
||||
mwc-list-item:hover .graphic .play {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
mwc-list-item .graphic .play.show {
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ============= Narrow ============= */
|
||||
|
||||
:host([narrow]) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .breadcrumb .title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .header_button {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content .img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
padding-bottom: 50%;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
background-position: center;
|
||||
border-radius: 0;
|
||||
transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
|
||||
}
|
||||
|
||||
mwc-fab {
|
||||
position: absolute;
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
bottom: -20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-info mwc-button {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-info {
|
||||
padding: 20px 24px 10px;
|
||||
}
|
||||
|
||||
:host([narrow]) .media-source,
|
||||
:host([narrow]) .children {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .children {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
/* ============= Scroll ============= */
|
||||
|
||||
:host([scroll]) .breadcrumb .subtitle {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:host([scroll]) .breadcrumb .title {
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
:host(:not([narrow])[scroll]) .header-info {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-info mwc-button,
|
||||
.no-img .header-info mwc-button {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
:host([scroll][narrow]) .no-img .header-info mwc-button {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-info {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-info mwc-button {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host([scroll][narrow]) .no-img .header-info {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
:host([scroll][narrow]) .header-info {
|
||||
padding: 20px 24px 10px 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-content {
|
||||
align-items: flex-end;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-content .img {
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
:host([scroll][narrow]) .header-content .img {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
padding-bottom: initial;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host([scroll]) mwc-fab {
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
--mdc-fab-box-shadow: none;
|
||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-browse": HaMediaPlayerBrowse;
|
||||
}
|
||||
}
|
27
src/components/media-player/show-media-browser-dialog.ts
Normal file
27
src/components/media-player/show-media-browser-dialog.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
mediaContentId?: string;
|
||||
mediaContentType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MediaPlayerBrowseDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-media-player-browse",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-media-player-browse" */ "./dialog-media-player-browse"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -322,9 +322,9 @@ export class PaperTimeInput extends PolymerElement {
|
||||
* @return {boolean}
|
||||
*/
|
||||
validate() {
|
||||
var valid = true;
|
||||
let valid = true;
|
||||
// Validate hour & min fields
|
||||
if (!this.$.hour.validate() | !this.$.min.validate()) {
|
||||
if (!this.$.hour.validate() || !this.$.min.validate()) {
|
||||
valid = false;
|
||||
}
|
||||
// Validate second field
|
||||
|
@@ -19,6 +19,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
</style>
|
||||
<ha-chart-base
|
||||
id="chart"
|
||||
hass="[[hass]]"
|
||||
data="[[chartData]]"
|
||||
identifier="[[identifier]]"
|
||||
rendered="{{rendered}}"
|
||||
@@ -28,6 +29,9 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
chartData: Object,
|
||||
data: Object,
|
||||
names: Object,
|
||||
|
@@ -25,6 +25,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
</style>
|
||||
<ha-chart-base
|
||||
hass="[[hass]]"
|
||||
data="[[chartData]]"
|
||||
rendered="{{rendered}}"
|
||||
rtl="{{rtl}}"
|
||||
@@ -75,6 +76,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
const staticColors = {
|
||||
on: 1,
|
||||
off: 0,
|
||||
home: 1,
|
||||
not_home: 0,
|
||||
unavailable: "#a0a0a0",
|
||||
unknown: "#606060",
|
||||
idle: 2,
|
||||
|
71
src/components/user/ha-person-badge.ts
Normal file
71
src/components/user/ha-person-badge.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { Person } from "../../data/person";
|
||||
import { computeInitials } from "./ha-user-badge";
|
||||
|
||||
@customElement("ha-person-badge")
|
||||
class PersonBadge extends LitElement {
|
||||
@property({ attribute: false }) public person?: Person;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.person) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const picture = this.person.picture;
|
||||
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({ backgroundImage: `url(${picture})` })}
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.person.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
${initials}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
.initials.long {
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-person-badge": PersonBadge;
|
||||
}
|
||||
}
|
@@ -3,17 +3,20 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { User } from "../../data/user";
|
||||
import { CurrentUser } from "../../types";
|
||||
import { CurrentUser, HomeAssistant } from "../../types";
|
||||
|
||||
const computeInitials = (name: string) => {
|
||||
export const computeInitials = (name: string) => {
|
||||
if (!name) {
|
||||
return "user";
|
||||
return "?";
|
||||
}
|
||||
return (
|
||||
name
|
||||
@@ -28,27 +31,86 @@ const computeInitials = (name: string) => {
|
||||
};
|
||||
|
||||
@customElement("ha-user-badge")
|
||||
class StateBadge extends LitElement {
|
||||
@property() public user?: User | CurrentUser;
|
||||
class UserBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const user = this.user;
|
||||
const initials = user ? computeInitials(user.name) : "?";
|
||||
return html` ${initials} `;
|
||||
}
|
||||
@property({ attribute: false }) public user?: User | CurrentUser;
|
||||
|
||||
@internalProperty() private _personPicture?: string;
|
||||
|
||||
private _personEntityId?: string;
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
toggleAttribute(
|
||||
this,
|
||||
"long",
|
||||
(this.user ? computeInitials(this.user.name) : "?").length > 2
|
||||
);
|
||||
if (changedProps.has("user")) {
|
||||
this._getPersonPicture();
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (
|
||||
this._personEntityId &&
|
||||
oldHass &&
|
||||
this.hass.states[this._personEntityId] !==
|
||||
oldHass.states[this._personEntityId]
|
||||
) {
|
||||
const state = this.hass.states[this._personEntityId];
|
||||
if (state) {
|
||||
this._personPicture = state.attributes.entity_picture;
|
||||
} else {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
} else if (!this._personEntityId && oldHass) {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.user) {
|
||||
return html``;
|
||||
}
|
||||
const picture = this._personPicture;
|
||||
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({ backgroundImage: `url(${picture})` })}
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.user.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
${initials}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getPersonPicture() {
|
||||
this._personEntityId = undefined;
|
||||
this._personPicture = undefined;
|
||||
if (!this.hass || !this.user) {
|
||||
return;
|
||||
}
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
if (
|
||||
entity.attributes.user_id === this.user.id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._personEntityId = entity.entity_id;
|
||||
this._personPicture = entity.attributes.entity_picture;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
@@ -57,11 +119,10 @@ class StateBadge extends LitElement {
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([long]) {
|
||||
.initials.long {
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
@@ -70,6 +131,6 @@ class StateBadge extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-user-badge": StateBadge;
|
||||
"ha-user-badge": UserBadge;
|
||||
}
|
||||
}
|
||||
|
@@ -53,7 +53,11 @@ class HaUserPicker extends LitElement {
|
||||
${this._sortedUsers(this.users).map(
|
||||
(user) => html`
|
||||
<paper-icon-item data-user-id=${user.id}>
|
||||
<ha-user-badge .user=${user} slot="item-icon"></ha-user-badge>
|
||||
<ha-user-badge
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
slot="item-icon"
|
||||
></ha-user-badge>
|
||||
${user.name}
|
||||
</paper-icon-item>
|
||||
`
|
||||
|
@@ -44,3 +44,14 @@ export const createAuthForUser = async (
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
export const adminChangePassword = async (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "config/auth_provider/homeassistant/admin_change_password",
|
||||
user_id: userId,
|
||||
password,
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HomeAssistant, Context } from "../types";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action } from "./script";
|
||||
|
||||
@@ -90,6 +90,12 @@ export interface ZoneTrigger {
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TagTrigger {
|
||||
platform: "tag";
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
export interface TimeTrigger {
|
||||
platform: "time";
|
||||
at: string;
|
||||
@@ -116,6 +122,7 @@ export type Trigger =
|
||||
| TimePatternTrigger
|
||||
| WebhookTrigger
|
||||
| ZoneTrigger
|
||||
| TagTrigger
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
@@ -199,3 +206,31 @@ export const getAutomationEditorInitData = () => {
|
||||
inititialAutomationEditorData = undefined;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const subscribeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
onChange: (result: {
|
||||
variables: {
|
||||
trigger: {};
|
||||
};
|
||||
context: Context;
|
||||
}) => void,
|
||||
trigger: Trigger | Trigger[],
|
||||
variables?: {}
|
||||
) =>
|
||||
hass.connection.subscribeMessage(onChange, {
|
||||
type: "subscribe_trigger",
|
||||
trigger,
|
||||
variables,
|
||||
});
|
||||
|
||||
export const testCondition = (
|
||||
hass: HomeAssistant,
|
||||
condition: Condition | Condition[],
|
||||
variables?: {}
|
||||
) =>
|
||||
hass.callWS<{ result: boolean }>({
|
||||
type: "test_condition",
|
||||
condition,
|
||||
variables,
|
||||
});
|
||||
|
@@ -9,14 +9,14 @@ interface CloudStatusBase {
|
||||
}
|
||||
|
||||
export interface GoogleEntityConfig {
|
||||
should_expose?: boolean;
|
||||
should_expose?: boolean | null;
|
||||
override_name?: string;
|
||||
aliases?: string[];
|
||||
disable_2fa?: boolean;
|
||||
}
|
||||
|
||||
export interface AlexaEntityConfig {
|
||||
should_expose?: boolean;
|
||||
should_expose?: boolean | null;
|
||||
}
|
||||
|
||||
export interface CertificateInformation {
|
||||
@@ -31,9 +31,11 @@ export interface CloudPreferences {
|
||||
remote_enabled: boolean;
|
||||
google_secure_devices_pin: string | undefined;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
google_default_expose: string[] | null;
|
||||
google_entity_configs: {
|
||||
[entityId: string]: GoogleEntityConfig;
|
||||
};
|
||||
alexa_default_expose: string[] | null;
|
||||
alexa_entity_configs: {
|
||||
[entityId: string]: AlexaEntityConfig;
|
||||
};
|
||||
@@ -106,8 +108,10 @@ export const updateCloudPref = (
|
||||
prefs: {
|
||||
google_enabled?: CloudPreferences["google_enabled"];
|
||||
alexa_enabled?: CloudPreferences["alexa_enabled"];
|
||||
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
|
||||
alexa_report_state?: CloudPreferences["alexa_report_state"];
|
||||
google_report_state?: CloudPreferences["google_report_state"];
|
||||
google_default_expose?: CloudPreferences["google_default_expose"];
|
||||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||
}
|
||||
) =>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user