Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
685d28229e Fix for displaying time left for values > 24 hour 2020-07-27 19:04:25 +02:00
437 changed files with 9269 additions and 28846 deletions

View File

@@ -1,7 +1,7 @@
{ {
"extends": [ "extends": [
"airbnb-typescript/base",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"airbnb-typescript/base",
"plugin:wc/recommended", "plugin:wc/recommended",
"plugin:lit/recommended", "plugin:lit/recommended",
"prettier", "prettier",
@@ -45,16 +45,16 @@
"func-names": 0, "func-names": 0,
"prefer-arrow-callback": 0, "prefer-arrow-callback": 0,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"no-var": 0,
"strict": 0, "strict": 0,
"prefer-spread": 0, "prefer-spread": 0,
"no-plusplus": 0, "no-plusplus": 0,
"no-bitwise": 2, "no-bitwise": 0,
"comma-dangle": 0, "comma-dangle": 0,
"vars-on-top": 0, "vars-on-top": 0,
"no-continue": 0, "no-continue": 0,
"no-param-reassign": 0, "no-param-reassign": 0,
"no-multi-assign": 0, "no-multi-assign": 0,
"no-console": 2,
"radix": 0, "radix": 0,
"no-alert": 0, "no-alert": 0,
"no-return-await": 0, "no-return-await": 0,

View File

@@ -1,60 +0,0 @@
name: "CodeQL"
on:
push:
branches: [dev, master]
pull_request:
# The branches below must be a subset of the branches above
branches: [dev]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -147,10 +147,6 @@
"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", "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" "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", "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" "name": "nfc-off"

View File

@@ -22,8 +22,6 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number; @property() public viewPath?: string | number;
public urlPath?: string | null;
protected render(): TemplateResult { protected render(): TemplateResult {
const index = this._viewIndex; const index = this._viewIndex;
if (index === undefined) { if (index === undefined) {
@@ -37,7 +35,6 @@ class HcLovelace extends LitElement {
const lovelace: Lovelace = { const lovelace: Lovelace = {
config: this.lovelaceConfig, config: this.lovelaceConfig,
editMode: false, editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
language: "en", language: "en",

View File

@@ -87,7 +87,6 @@ export class HcMain extends HassElement {
.hass=${this.hass} .hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig} .lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath} .viewPath=${this._lovelacePath}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig} @config-refresh=${this._generateLovelaceConfig}
></hc-lovelace> ></hc-lovelace>
`; `;

View File

@@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content"; import "../../../src/state-summary/state-card-content";
import "./more-info-content";
class DemoMoreInfo extends PolymerElement { class DemoMoreInfo extends PolymerElement {
static get template() { static get template() {

View File

@@ -1,73 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { property, PropertyValues, UpdatingElement } from "lit-element";
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type";
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
import "../../../src/dialogs/more-info/controls/more-info-automation";
import "../../../src/dialogs/more-info/controls/more-info-camera";
import "../../../src/dialogs/more-info/controls/more-info-climate";
import "../../../src/dialogs/more-info/controls/more-info-configurator";
import "../../../src/dialogs/more-info/controls/more-info-counter";
import "../../../src/dialogs/more-info/controls/more-info-cover";
import "../../../src/dialogs/more-info/controls/more-info-default";
import "../../../src/dialogs/more-info/controls/more-info-fan";
import "../../../src/dialogs/more-info/controls/more-info-group";
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
import "../../../src/dialogs/more-info/controls/more-info-light";
import "../../../src/dialogs/more-info/controls/more-info-lock";
import "../../../src/dialogs/more-info/controls/more-info-media_player";
import "../../../src/dialogs/more-info/controls/more-info-person";
import "../../../src/dialogs/more-info/controls/more-info-script";
import "../../../src/dialogs/more-info/controls/more-info-sun";
import "../../../src/dialogs/more-info/controls/more-info-timer";
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
import "../../../src/dialogs/more-info/controls/more-info-weather";
import { HomeAssistant } from "../../../src/types";
class MoreInfoContent extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
protected firstUpdated(): void {
this.style.position = "relative";
this.style.display = "block";
}
// This is not a lit element, but an updating element, so we implement update
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) {
if (this.lastChild) {
this._detachedChild = this.lastChild;
// Detach child to prevent it from doing work.
this.removeChild(this.lastChild);
}
return;
}
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = undefined;
}
const moreInfoType =
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
? stateObj.attributes.custom_ui_more_info
: "more-info-" + stateMoreInfoType(stateObj);
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass,
stateObj,
});
}
}
customElements.define("more-info-content", MoreInfoContent);

View File

@@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity"; import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos"; import "../components/demo-more-infos";
import "../components/more-info-content";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {

View File

@@ -1,13 +1,12 @@
import "@material/mwc-icon-button/mwc-icon-button"; 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 "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { import {
css, css,
CSSResult, CSSResult,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
@@ -20,13 +19,13 @@ import {
HassioAddonRepository, HassioAddonRepository,
reloadHassioAddons, reloadHassioAddons,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") { if (a.slug === "local") {
@@ -180,7 +179,7 @@ class HassioAddonStore extends LitElement {
this._repos.sort(sortRepos); this._repos.sort(sortRepos);
this._addons = addonsInfo.addons; this._addons = addonsInfo.addons;
} catch (err) { } catch (err) {
alert(extractApiErrorMessage(err)); alert("Failed to fetch add-on info");
} }
} }

View File

@@ -28,7 +28,6 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/buttons/ha-progress-button";
@customElement("hassio-addon-audio") @customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement { class HassioAddonAudio extends LitElement {
@@ -92,9 +91,7 @@ class HassioAddonAudio extends LitElement {
</paper-dropdown-menu> </paper-dropdown-menu>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._saveSettings}> <mwc-button @click=${this._saveSettings}>Save</mwc-button>
Save
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
@@ -175,10 +172,7 @@ class HassioAddonAudio extends LitElement {
} }
} }
private async _saveSettings(ev: CustomEvent): Promise<void> { private async _saveSettings(): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
audio_input: audio_input:
@@ -188,14 +182,12 @@ class HassioAddonAudio extends LitElement {
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch { } catch {
this._error = "Failed to set addon audio device"; this._error = "Failed to set addon audio device";
} }
if (!this._error && this.addon?.state === "started") {
button.progress = false; await suggestAddonRestart(this, this.hass, this.addon);
}
} }
} }

View File

@@ -5,15 +5,14 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor"; import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -22,7 +21,6 @@ import {
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -57,103 +55,20 @@ class HassioAddonConfig extends LitElement {
${valid ? "" : html` <div class="errors">Invalid YAML</div> `} ${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}> <mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults Reset to defaults
</ha-progress-button> </mwc-button>
<ha-progress-button <mwc-button
@click=${this._saveTapped} @click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid} .disabled=${!this._configHasChanged || !valid}
> >
Save Save
</ha-progress-button> </mwc-button>
</div> </div>
</ha-card> </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[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
@@ -183,6 +98,80 @@ 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 { declare global {

View File

@@ -4,21 +4,19 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import { import {
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -87,17 +85,38 @@ class HassioAddonNetwork extends LitElement {
</table> </table>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}> <mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults Reset to defaults
</ha-progress-button> </mwc-button>
<ha-progress-button @click=${this._saveTapped}> <mwc-button @click=${this._saveTapped}>Save</mwc-button>
Save
</ha-progress-button>
</div> </div>
</ha-card> </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 { protected update(changedProperties: PropertyValues): void {
super.update(changedProperties); super.update(changedProperties);
if (changedProperties.has("addon")) { if (changedProperties.has("addon")) {
@@ -130,10 +149,7 @@ class HassioAddonNetwork extends LitElement {
}); });
} }
private async _resetTapped(ev: CustomEvent): Promise<void> { private async _resetTapped(): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
network: null, network: null,
}; };
@@ -146,22 +162,17 @@ class HassioAddonNetwork extends LitElement {
path: "option", path: "option",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") { } 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); await suggestAddonRestart(this, this.hass, this.addon);
} }
} catch (err) {
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; this._error = undefined;
const networkconfiguration = {}; const networkconfiguration = {};
this._config!.forEach((item) => { this._config!.forEach((item) => {
@@ -180,38 +191,14 @@ class HassioAddonNetwork extends LitElement {
path: "option", path: "option",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") { } 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); await suggestAddonRestart(this, this.hass, this.addon);
} }
} catch (err) {
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;
}
`,
];
} }
} }

View File

@@ -3,19 +3,18 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import { import {
fetchHassioAddonDocumentation, fetchHassioAddonDocumentation,
HassioAddonDetails, HassioAddonDetails,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
@@ -81,9 +80,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug this.addon!.slug
); );
} catch (err) { } catch (err) {
this._error = `Failed to get addon documentation, ${extractApiErrorMessage( this._error = `Failed to get addon documentation, ${
err err.body?.message || err
)}`; }`;
} }
} }
} }

View File

@@ -9,19 +9,21 @@ import {
mdiExclamationThick, mdiExclamationThick,
mdiFlask, mdiFlask,
mdiHomeAssistant, mdiHomeAssistant,
mdiInformation,
mdiKey, mdiKey,
mdiNetwork, mdiNetwork,
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
@@ -33,27 +35,19 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge"; import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import { import {
fetchHassioAddonChangelog, fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
HassioAddonSetSecurityParams, HassioAddonSetSecurityParams,
installHassioAddon, installHassioAddon,
setHassioAddonOption, setHassioAddonOption,
setHassioAddonSecurity, setHassioAddonSecurity,
startHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; 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 { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
@@ -133,6 +127,8 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@property({ type: Boolean }) private _installing = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this._computeUpdateAvailable ${this._computeUpdateAvailable
@@ -246,9 +242,9 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<div class="security"> <div class="security">
${this.addon.stage !== "stable" <ha-label-badge
? html` <ha-label-badge
class=${classMap({ class=${classMap({
green: this.addon.stage === "stable",
yellow: this.addon.stage === "experimental", yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated", red: this.addon.stage === "deprecated",
})} })}
@@ -257,12 +253,8 @@ class HassioAddonInfo extends LitElement {
label="stage" label="stage"
description="" description=""
> >
<ha-svg-icon <ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
.path=${STAGE_ICON[this.addon.stage]} </ha-label-badge>
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge <ha-label-badge
class=${classMap({ class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)), green: [5, 6].includes(Number(this.addon.rating)),
@@ -390,94 +382,67 @@ class HassioAddonInfo extends LitElement {
${this.addon.version ${this.addon.version
? html` ? html`
<div class="addon-options"> <div class="state">
<ha-settings-row ?three-line=${this.narrow}> <div>Start on boot</div>
<span slot="heading">
Start on boot
</span>
<span slot="description">
Make the add-on start during a system boot
</span>
<ha-switch <ha-switch
@change=${this._startOnBootToggled} @change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"} .checked=${this.addon.boot === "auto"}
haptic haptic
></ha-switch> ></ha-switch>
</ha-settings-row> </div>
${this.addon.startup !== "once"
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Watchdog
</span>
<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 ${this.addon.auto_update || this.hass.userData?.showAdvanced
? html` ? html`
<ha-settings-row ?three-line=${this.narrow}> <div class="state">
<span slot="heading"> <div>Auto update</div>
Auto update
</span>
<span slot="description">
Auto update the add-on when there is a new version
available
</span>
<ha-switch <ha-switch
@change=${this._autoUpdateToggled} @change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update} .checked=${this.addon.auto_update}
haptic haptic
></ha-switch> ></ha-switch>
</ha-settings-row> </div>
` `
: ""} : ""}
${this.addon.ingress ${this.addon.ingress
? html` ? html`
<ha-settings-row ?three-line=${this.narrow}> <div class="state">
<span slot="heading"> <div>Show in sidebar</div>
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 <ha-switch
@change=${this._panelToggled} @change=${this._panelToggled}
.checked=${this.addon.ingress_panel} .checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar} .disabled=${this._computeCannotIngressSidebar}
haptic haptic
></ha-switch> ></ha-switch>
</ha-settings-row> ${this._computeCannotIngressSidebar
? html`
<span>
This option requires Home Assistant 0.92 or
later.
</span>
`
: ""}
</div>
` `
: ""} : ""}
${this._computeUsesProtectedOptions ${this._computeUsesProtectedOptions
? html` ? html`
<ha-settings-row ?three-line=${this.narrow}> <div class="state">
<span slot="heading"> <div>
Protection mode Protection mode
<span>
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
</span> </span>
<span slot="description"> </div>
Blocks elevated system access from the add-on
</span>
<ha-switch <ha-switch
@change=${this._protectionToggled} @change=${this._protectionToggled}
.checked=${this.addon.protected} .checked=${this.addon.protected}
haptic haptic
></ha-switch> ></ha-switch>
</ha-settings-row> </div>
` `
: ""} : ""}
</div>
` `
: ""} : ""}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -503,9 +468,12 @@ class HassioAddonInfo extends LitElement {
</ha-call-api-button> </ha-call-api-button>
` `
: html` : html`
<ha-progress-button @click=${this._startClicked}> <ha-call-api-button
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start Start
</ha-progress-button> </ha-call-api-button>
`} `}
${this._computeShowWebUI ${this._computeShowWebUI
? html` ? html`
@@ -529,12 +497,12 @@ class HassioAddonInfo extends LitElement {
</mwc-button> </mwc-button>
` `
: ""} : ""}
<ha-progress-button <mwc-button
class=" right warning" class=" right warning"
@click=${this._uninstallClicked} @click=${this._uninstallClicked}
> >
Uninstall Uninstall
</ha-progress-button> </mwc-button>
${this.addon.build ${this.addon.build
? html` ? html`
<ha-call-api-button <ha-call-api-button
@@ -556,7 +524,8 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<ha-progress-button <ha-progress-button
.disabled=${!this.addon.available} .disabled=${!this.addon.available || this._installing}
.progress=${this._installing}
@click=${this._installClicked} @click=${this._installClicked}
> >
Install Install
@@ -579,6 +548,137 @@ 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 { private get _computeHassioApi(): boolean {
return ( return (
this.addon.hassio_api && this.addon.hassio_api &&
@@ -663,29 +763,7 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage( this._error = `Failed to set addon option, ${err.body?.message || err}`;
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
)}`;
} }
} }
@@ -703,9 +781,7 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage( this._error = `Failed to set addon option, ${err.body?.message || err}`;
err
)}`;
} }
} }
@@ -723,9 +799,9 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon security option, ${extractApiErrorMessage( this._error = `Failed to set addon security option, ${
err err.body?.message || err
)}`; }`;
} }
} }
@@ -743,13 +819,12 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage( this._error = `Failed to set addon option, ${err.body?.message || err}`;
err
)}`;
} }
} }
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
this._error = undefined;
try { try {
const content = await fetchHassioAddonChangelog( const content = await fetchHassioAddonChangelog(
this.hass, this.hass,
@@ -760,17 +835,15 @@ class HassioAddonInfo extends LitElement {
content, content,
}); });
} catch (err) { } catch (err) {
showAlertDialog(this, { this._error = `Failed to get addon changelog, ${
title: "Failed to get addon changelog", err.body?.message || err
text: extractApiErrorMessage(err), }`;
});
} }
} }
private async _installClicked(ev: CustomEvent): Promise<void> { private async _installClicked(): Promise<void> {
const button = ev.currentTarget as any; this._error = undefined;
button.progress = true; this._installing = true;
try { try {
await installHassioAddon(this.hass, this.addon.slug); await installHassioAddon(this.hass, this.addon.slug);
const eventdata = { const eventdata = {
@@ -780,62 +853,12 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
showAlertDialog(this, { this._error = `Failed to install addon, ${err.body?.message || err}`;
title: "Failed to install addon",
text: extractApiErrorMessage(err),
});
} }
button.progress = false; this._installing = false;
} }
private async _startClicked(ev: CustomEvent): Promise<void> { private async _uninstallClicked(): 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, { const confirmed = await showConfirmationDialog(this, {
title: this.addon.name, title: this.addon.name,
text: "Are you sure you want to uninstall this add-on?", text: "Are you sure you want to uninstall this add-on?",
@@ -844,7 +867,6 @@ class HassioAddonInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
@@ -858,152 +880,8 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
showAlertDialog(this, { this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
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 { declare global {

View File

@@ -4,9 +4,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
@@ -14,7 +14,6 @@ import {
fetchHassioAddonLogs, fetchHassioAddonLogs,
HassioAddonDetails, HassioAddonDetails,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html"; import "../../components/hassio-ansi-to-html";
@@ -76,7 +75,7 @@ class HassioAddonLogs extends LitElement {
try { try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) { } catch (err) {
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`; this._error = `Failed to get addon logs, ${err.body?.message || err}`;
} }
} }

View File

@@ -21,7 +21,7 @@ interface State {
class HassioAnsiToHtml extends LitElement { class HassioAnsiToHtml extends LitElement {
@property() public content!: string; @property() public content!: string;
protected render(): TemplateResult | void { public render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`; return html`${this._parseTextToColoredPre(this.content)}`;
} }

View File

@@ -5,28 +5,19 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import { import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@@ -135,47 +126,32 @@ export class HassioUpdate extends LitElement {
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer"> <a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button> <mwc-button>Release notes</mwc-button>
</a> </a>
<ha-progress-button <ha-call-api-button
.apiPath=${apiPath} .hass=${this.hass}
.name=${name} .path=${apiPath}
.version=${lastVersion} @hass-api-called=${this._apiCalled}
@click=${this._confirmUpdate}
> >
Update Update
</ha-progress-button> </ha-call-api-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _confirmUpdate(ev): Promise<void> { private _apiCalled(ev): void {
const item = ev.currentTarget; if (ev.detail.success) {
item.progress = true; this._error = "";
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; return;
} }
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath); const response = ev.detail.response;
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy) if (typeof response.body === "object") {
// or no status at all(connection terminated) this._error = response.body.message || "Unknown error";
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) { } else {
showAlertDialog(this, { this._error = response.body;
title: "Update failed",
text: extractApiErrorMessage(err),
});
} }
} }
item.progress = false;
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [

View File

@@ -31,10 +31,6 @@ class HassioMarkdownDialog extends LitElement {
this._opened = true; this._opened = true;
} }
public closeDialog() {
this._opened = false;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._opened) { if (!this._opened) {
return html``; return html``;
@@ -42,7 +38,7 @@ class HassioMarkdownDialog extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
open open
@closed=${this.closeDialog} @closing=${this._closeDialog}
.heading=${createCloseHeading(this.hass, this.title)} .heading=${createCloseHeading(this.hass, this.title)}
> >
<ha-markdown .content=${this.content || ""}></ha-markdown> <ha-markdown .content=${this.content || ""}></ha-markdown>
@@ -50,6 +46,10 @@ class HassioMarkdownDialog extends LitElement {
`; `;
} }
private _closeDialog(): void {
this._opened = false;
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyleDialog, haStyleDialog,

View File

@@ -1,333 +0,0 @@
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;
}
}

View File

@@ -1,22 +0,0 @@
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,
});
};

View File

@@ -5,26 +5,25 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@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";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "../../../../src/components/ha-circular-progress";
import { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { import {
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
HassioAddonRepository, HassioAddonRepository,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -191,7 +190,7 @@ class HassioRepositoriesDialog extends LitElement {
input.value = ""; input.value = "";
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = err.message;
} }
this._prosessing = false; this._prosessing = false;
} }
@@ -223,7 +222,7 @@ class HassioRepositoriesDialog extends LitElement {
await this._dialogParams!.loadData(); await this._dialogParams!.loadData();
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = err.message;
} }
} }
} }

View File

@@ -7,20 +7,18 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
fetchHassioSnapshotInfo, fetchHassioSnapshotInfo,
HassioSnapshotDetail, HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot"; } from "../../../../src/data/hassio/snapshot";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../../src/polymer-types"; import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyleDialog } from "../../../../src/resources/styles"; import { haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
@@ -268,12 +266,8 @@ class HassioSnapshotDialog extends LitElement {
this._snapshotPassword = ev.detail.value; this._snapshotPassword = ev.detail.value;
} }
private async _partialRestoreClicked() { private _partialRestoreClicked() {
if ( if (!confirm("Are you sure you want to restore this snapshot?")) {
!(await showConfirmationDialog(this, {
title: "Are you sure you want partially to restore this snapshot?",
}))
) {
return; return;
} }
@@ -318,13 +312,8 @@ class HassioSnapshotDialog extends LitElement {
); );
} }
private async _fullRestoreClicked() { private _fullRestoreClicked() {
if ( if (!confirm("Are you sure you want to restore this snapshot?")) {
!(await showConfirmationDialog(this, {
title:
"Are you sure you want to wipe your system and restore this snapshot?",
}))
) {
return; return;
} }
@@ -349,12 +338,8 @@ class HassioSnapshotDialog extends LitElement {
); );
} }
private async _deleteClicked() { private _deleteClicked() {
if ( if (!confirm("Are you sure you want to delete this snapshot?")) {
!(await showConfirmationDialog(this, {
title: "Are you sure you want to delete this snapshot?",
}))
) {
return; return;
} }
@@ -380,7 +365,7 @@ class HassioSnapshotDialog extends LitElement {
`/api/hassio/snapshots/${this._snapshot!.slug}/download` `/api/hassio/snapshots/${this._snapshot!.slug}/download`
); );
} catch (err) { } catch (err) {
alert(`Error: ${extractApiErrorMessage(err)}`); alert(`Error: ${err.message}`);
return; return;
} }

View File

@@ -3,7 +3,6 @@ import {
HassioAddonDetails, HassioAddonDetails,
restartHassioAddon, restartHassioAddon,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -27,7 +26,7 @@ export const suggestAddonRestart = async (
} catch (err) { } catch (err) {
showAlertDialog(element, { showAlertDialog(element, {
title: "Failed to restart", title: "Failed to restart",
text: extractApiErrorMessage(err), text: err.body.message,
}); });
} }
} }

View File

@@ -1,35 +1,116 @@
import { PolymerElement } from "@polymer/polymer";
import { import {
html,
PropertyValues,
customElement, customElement,
LitElement,
property, property,
internalProperty,
PropertyValues,
} from "lit-element"; } 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 { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event"; 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 { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { atLeastVersion } from "../../src/common/config/version"; 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";
@customElement("hassio-main") @customElement("hassio-main")
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo; @property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public route?: Route; 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) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._applyTheme(); applyThemesOnElement(
this.parentElement,
this.hass.themes,
this.hass.selectedTheme || this.hass.themes.default_theme
);
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 // Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the // 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. // supervisor UI can also run under older versions of Home Assistant.
@@ -62,61 +143,152 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
}); });
}); });
makeDialogManager(this, this.shadowRoot!); makeDialogManager(this, document.body);
} }
protected updated(changedProps: PropertyValues) { protected updatePageEl(el) {
super.updated(changedProps); // the tabs page does its own routing so needs full route.
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
if (!oldHass) {
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,
});
} 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; return;
} }
if (oldHass.themes !== this.hass.themes) {
this._applyTheme(); 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);
} }
} }
protected render() { private async _redirectIngress(addonSlug: string) {
return html` // When we trigger a navigation, we sleep to make sure we don't
<hassio-router // show the hassio dashboard before navigating away.
.hass=${this.hass} const awaitAlert = async (
.route=${this.route} alertParams: AlertDialogParams,
.panel=${this.panel} action: () => void
.narrow=${this.narrow} ) => {
></hassio-router> await new Promise((resolve) => {
`; alertParams.confirm = resolve;
} showAlertDialog(this, alertParams);
});
private _applyTheme() { action();
let themeName: string; await new Promise((resolve) => setTimeout(resolve, 1000));
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 { const createSessionPromise = createHassioSession(this.hass).then(
themeName = () => true,
((this.hass.selectedTheme as unknown) as string) || () => false
this.hass.themes.default_theme; );
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;
} }
applyThemesOnElement( if (!addon.ingress_url) {
this.parentElement, await awaitAlert(
this.hass.themes, {
themeName, text: "Add-on does not support Ingress",
options 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();
} }
} }

View File

@@ -4,8 +4,6 @@ import {
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import { import {
@@ -35,9 +33,6 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.supervisorInfo) {
return html``;
}
return html` return html`
<hassio-panel-router <hassio-panel-router
.route=${this.route} .route=${this.route}
@@ -51,16 +46,6 @@ class HassioPanel extends LitElement {
></hassio-panel-router> ></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 { declare global {

View File

@@ -1,150 +0,0 @@
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;
}
}

View File

@@ -17,10 +17,6 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types"; 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") @customElement("hassio-ingress-view")
class HassioIngressView extends LitElement { class HassioIngressView extends LitElement {
@@ -28,45 +24,22 @@ class HassioIngressView extends LitElement {
@property() public route!: Route; @property() public route!: Route;
@property() public ingressPanel = false;
@internalProperty() private _addon?: HassioAddonDetails; @internalProperty() private _addon?: HassioAddonDetails;
@property({ type: Boolean })
public narrow = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._addon) { if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`; return html`
<hass-subpage .header=${this._addon.name} hassio>
if (!this.ingressPanel) { <iframe src=${this._addon.ingress_url}></iframe>
return html`<hass-subpage </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) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.firstUpdated(changedProps);
if (!changedProps.has("route")) { if (!changedProps.has("route")) {
return; return;
@@ -83,56 +56,27 @@ class HassioIngressView extends LitElement {
} }
private async _fetchData(addonSlug: string) { private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try { try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug); const [addon] = await Promise.all([
} catch (err) { fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
await showAlertDialog(this, { throw new Error("Failed to fetch add-on info");
text: "Unable to fetch add-on info to start Ingress", }),
title: "Supervisor", createHassioSession(this.hass).catch(() => {
}); throw new Error("Failed to create an ingress session");
history.back(); }),
return; ]);
}
if (!addon.ingress_url) { if (!addon.ingress) {
await showAlertDialog(this, { throw new Error("This add-on does not support ingress");
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; this._addon = addon;
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert(err.message || "Unknown error starting ingress.");
history.back();
} }
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
} }
static get styles(): CSSResult { static get styles(): CSSResult {
@@ -143,41 +87,6 @@ class HassioIngressView extends LitElement {
height: 100%; height: 100%;
border: 0; 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);
}
`; `;
} }
} }

View File

@@ -13,17 +13,15 @@ import {
CSSResultArray, CSSResultArray,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
createHassioFullSnapshot, createHassioFullSnapshot,
createHassioPartialSnapshot, createHassioPartialSnapshot,
@@ -82,6 +80,8 @@ class HassioSnapshots extends LitElement {
{ slug: "addons/local", name: "Local add-ons", checked: true }, { slug: "addons/local", name: "Local add-ons", checked: true },
]; ];
@internalProperty() private _creatingSnapshot = false;
@internalProperty() private _error = ""; @internalProperty() private _error = "";
public async refreshData() { public async refreshData() {
@@ -192,9 +192,12 @@ class HassioSnapshots extends LitElement {
: undefined} : undefined}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._createSnapshot}> <mwc-button
.disabled=${this._creatingSnapshot}
@click=${this._createSnapshot}
>
Create Create
</ha-progress-button> </mwc-button>
</div> </div>
</ha-card> </ha-card>
</div> </div>
@@ -227,7 +230,7 @@ class HassioSnapshots extends LitElement {
.icon=${snapshot.type === "full" .icon=${snapshot.type === "full"
? mdiPackageVariantClosed ? mdiPackageVariantClosed
: mdiPackageVariant} : mdiPackageVariant}
icon-class="snapshot" .icon-class="snapshot"
></hassio-card-content> ></hassio-card-content>
</div> </div>
</ha-card> </ha-card>
@@ -290,20 +293,17 @@ class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass); this._snapshots = await fetchHassioSnapshots(this.hass);
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1)); this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = err.message;
} }
} }
private async _createSnapshot(ev: CustomEvent): Promise<void> { private async _createSnapshot() {
const button = ev.currentTarget as any;
button.progress = true;
this._error = ""; this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) { if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password."; this._error = "Please enter a password.";
button.progress = false;
return; return;
} }
this._creatingSnapshot = true;
await this.updateComplete; await this.updateComplete;
const name = const name =
@@ -343,9 +343,10 @@ class HassioSnapshots extends LitElement {
this._updateSnapshots(); this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null }); fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = err.message;
} finally {
this._creatingSnapshot = false;
} }
button.progress = false;
} }
private _computeDetails(snapshot: HassioSnapshot) { private _computeDetails(snapshot: HassioSnapshot) {

View File

@@ -1,32 +1,18 @@
import "@material/mwc-button"; 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 { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import "../../../src/components/buttons/ha-call-api-button";
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,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { import {
changeHostOptions, changeHostOptions,
configSyncOS,
fetchHassioHostInfo, fetchHassioHostInfo,
HassioHassOSInfo, HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType, HassioHostInfo as HassioHostInfoType,
@@ -34,10 +20,6 @@ import {
shutdownHost, shutdownHost,
updateOS, updateOS,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import {
fetchNetworkInfo,
NetworkInfo,
} from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor"; import { HassioInfo } from "../../../src/data/hassio/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -47,7 +29,6 @@ import {
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info") @customElement("hassio-host-info")
@@ -60,179 +41,164 @@ class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@internalProperty() public _networkInfo?: NetworkInfo; @internalProperty() private _errors?: string;
protected render(): TemplateResult | void { public render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!)
: "";
return html` return html`
<ha-card header="Host System"> <ha-card>
<div class="card-content"> <div class="card-content">
${this.hostInfo.features.includes("hostname") <h2>Host system</h2>
? html`<ha-settings-row> <table class="info">
<span slot="heading"> <tbody>
Hostname <tr>
</span> <td>Hostname</td>
<span slot="description"> <td>${this.hostInfo.hostname}</td>
${this.hostInfo.hostname} </tr>
</span> <tr>
<mwc-button <td>System</td>
title="Change the hostname" <td>${this.hostInfo.operating_system}</td>
label="Change" </tr>
@click=${this._changeHostnameClicked}
>
</mwc-button>
</ha-settings-row>`
: ""}
${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") ${!this.hostInfo.features.includes("hassos")
? html`<ha-settings-row> ? html`<tr>
<span slot="heading"> <td>Docker version</td>
Docker version <td>${this.hassioInfo.docker}</td>
</span> </tr>`
<span slot="description">
${this.hassioInfo.docker}
</span>
</ha-settings-row>`
: ""} : ""}
${this.hostInfo.deployment ${this.hostInfo.deployment
? html`<ha-settings-row> ? html`
<span slot="heading"> <tr>
Deployment <td>Deployment</td>
</span> <td>${this.hostInfo.deployment}</td>
<span slot="description"> </tr>
${this.hostInfo.deployment} `
</span> : ""}
</ha-settings-row>` </tbody>
</table>
<mwc-button raised @click=${this._showHardware} class="info">
Hardware
</mwc-button>
${this.hostInfo.features.includes("hostname")
? html`
<mwc-button
raised
@click=${this._changeHostnameClicked}
class="info"
>
Change hostname
</mwc-button>
`
: ""}
${this._errors
? html` <div class="errors">Error: ${this._errors}</div> `
: ""} : ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
${this.hostInfo.features.includes("reboot") ${this.hostInfo.features.includes("reboot")
? html` ? html`
<ha-progress-button <mwc-button class="warning" @click=${this._rebootHost}
title="Reboot the host OS" >Reboot</mwc-button
class="warning"
@click=${this._hostReboot}
> >
Reboot
</ha-progress-button>
` `
: ""} : ""}
${this.hostInfo.features.includes("shutdown") ${this.hostInfo.features.includes("shutdown")
? html` ? html`
<ha-progress-button <mwc-button class="warning" @click=${this._shutdownHost}
title="Shutdown the host OS" >Shutdown</mwc-button
class="warning"
@click=${this._hostShutdown}
> >
Shutdown
</ha-progress-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") ${this.hostInfo.features.includes("hassos")
? html`<mwc-list-item ? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/os/config/sync"
title="Load HassOS configs or updates from USB" title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
> >
Import from USB `
</mwc-list-item>` : ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
: ""} : ""}
</ha-button-menu>
</div> </div>
</ha-card> </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 { protected firstUpdated(): void {
this._loadData(); this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
} }
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { private _apiCalled(ev): void {
if (!network_info) { if (ev.detail.success) {
return ""; this._errors = undefined;
return;
} }
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { const response = ev.detail.response;
switch (ev.detail.index) {
case 0: this._errors =
await this._showHardware(); typeof response.body === "object"
break; ? response.body.message || "Unknown error"
case 1: : response.body;
await this._importFromUSB();
break;
}
} }
private async _showHardware(): Promise<void> { private async _showHardware(): Promise<void> {
try { try {
const content = await fetchHassioHardwareInfo(this.hass); const content = this._objectToMarkdown(
await fetchHassioHardwareInfo(this.hass)
);
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: "Hardware", title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`, content,
}); });
} catch (err) { } catch (err) {
showAlertDialog(this, { showHassioMarkdownDialog(this, {
title: "Failed to get Hardware list", title: "Hardware",
text: extractApiErrorMessage(err), content: "Error getting hardware info",
}); });
} }
} }
private async _hostReboot(ev: CustomEvent): Promise<void> { private async _rebootHost(): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: "Reboot", title: "Reboot",
text: "Are you sure you want to reboot the host?", text: "Are you sure you want to reboot the host?",
@@ -241,28 +207,20 @@ class HassioHostInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
try { try {
await rebootHost(this.hass); await rebootHost(this.hass);
} catch (err) { } catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to reboot", title: "Failed to reboot",
text: extractApiErrorMessage(err), text: err.body.message,
}); });
} }
} }
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _shutdownHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: "Shutdown", title: "Shutdown",
text: "Are you sure you want to shutdown the host?", text: "Are you sure you want to shutdown the host?",
@@ -271,28 +229,20 @@ class HassioHostInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
try { try {
await shutdownHost(this.hass); await shutdownHost(this.hass);
} catch (err) { } catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to shutdown", title: "Failed to shutdown",
text: extractApiErrorMessage(err), text: err.body.message,
}); });
} }
} }
button.progress = false;
}
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _updateOS(): Promise<void> {
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: "Update", title: "Update",
text: "Are you sure you want to update the OS?", text: "Are you sure you want to update the OS?",
@@ -301,7 +251,6 @@ class HassioHostInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
@@ -310,17 +259,30 @@ class HassioHostInfo extends LitElement {
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to update", title: "Failed to update",
text: extractApiErrorMessage(err), text: err.body.message,
}); });
} }
button.progress = false;
} }
private async _changeNetworkClicked(): Promise<void> { private _objectToMarkdown(obj, indent = ""): string {
showNetworkDialog(this, { let data = "";
network: this._networkInfo!, Object.keys(obj).forEach((key) => {
loadData: () => this._loadData(), 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}`);
}
}
}); });
return data;
} }
private async _changeHostnameClicked(): Promise<void> { private async _changeHostnameClicked(): Promise<void> {
@@ -339,83 +301,11 @@ class HassioHostInfo extends LitElement {
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Setting hostname failed", title: "Setting hostname failed",
text: extractApiErrorMessage(err), text: err.body.message,
}); });
} }
} }
} }
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 { declare global {

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { import {
css, css,
CSSResult, CSSResult,
@@ -5,29 +6,21 @@ import {
html, html,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card"; 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 { import {
HassioSupervisorInfo as HassioSupervisorInfoType, HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement { class HassioSupervisorInfo extends LitElement {
@@ -35,120 +28,122 @@ class HassioSupervisorInfo extends LitElement {
@property() public supervisorInfo!: HassioSupervisorInfoType; @property() public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType; @internalProperty() private _errors?: string;
protected render(): TemplateResult | void { public render(): TemplateResult | void {
return html` return html`
<ha-card header="Supervisor"> <ha-card>
<div class="card-content"> <div class="card-content">
<ha-settings-row> <h2>Supervisor</h2>
<span slot="heading"> <table class="info">
Version <tbody>
</span> <tr>
<span slot="description"> <td>Version</td>
${this.supervisorInfo.version} <td>${this.supervisorInfo.version}</td>
</span> </tr>
</ha-settings-row> <tr>
<ha-settings-row> <td>Latest version</td>
<span slot="heading"> <td>${this.supervisorInfo.version_latest}</td>
Newest version </tr>
</span> ${this.supervisorInfo.channel !== "stable"
<span slot="description">
${this.supervisorInfo.version_latest}
</span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html` ? html`
<ha-progress-button <tr>
title="Update the supervisor" <td>Channel</td>
@click=${this._supervisorUpdate} <td>${this.supervisorInfo.channel}</td>
> </tr>
Update
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </tbody>
<ha-settings-row> </table>
<span slot="heading"> ${this._errors
Channel ? html` <div class="errors">Error: ${this._errors}</div> `
</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>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
@click=${this._supervisorReload} >Reload</ha-call-api-button
title="Reload parts of the supervisor."
> >
Reload ${this.supervisorInfo.version !== this.supervisorInfo.version_latest
</ha-progress-button> ? 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
>
`
: ""}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _toggleBeta(ev: CustomEvent): Promise<void> { static get styles(): CSSResult[] {
const button = ev.currentTarget as any; return [
button.progress = true; 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;
}
`,
];
}
if (this.supervisorInfo.channel === "stable") { 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, { const confirmed = await showConfirmationDialog(this, {
title: "WARNING", title: "WARNING",
text: html` Beta releases are for testers and early adopters and can text: html` Beta releases are for testers and early adopters and can
@@ -170,135 +165,22 @@ class HassioSupervisorInfo extends LitElement {
}); });
if (!confirmed) { 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);
this.supervisorInfo = await fetchHassioSupervisorInfo(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);
this.supervisorInfo = await fetchHassioSupervisorInfo(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; return;
} }
try { try {
await updateSupervisor(this.hass); const data: SupervisorOptions = { channel: "beta" };
} 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); await setSupervisorOption(this.hass, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
showAlertDialog(this, { this._errors = `Error joining beta channel, ${err.body?.message || err}`;
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
} }
} }
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;
}
button.link {
color: var(--primary-color);
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
`,
];
}
} }
declare global { declare global {

View File

@@ -7,14 +7,12 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -69,7 +67,7 @@ class HassioSupervisorLog extends LitElement {
await this._loadData(); await this._loadData();
} }
protected render(): TemplateResult | void { public render(): TemplateResult | void {
return html` return html`
<ha-card> <ha-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -104,49 +102,18 @@ class HassioSupervisorLog extends LitElement {
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`} : html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._refresh}> <mwc-button @click=${this._refresh}>Refresh</mwc-button>
Refresh
</ha-progress-button>
</div> </div>
</ha-card> </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[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
hassioStyle, hassioStyle,
css` css`
ha-card { ha-card {
margin-top: 8px;
width: 100%; width: 100%;
} }
pre { pre {
@@ -160,9 +127,38 @@ class HassioSupervisorLog extends LitElement {
color: var(--error-color); color: var(--error-color);
margin-bottom: 16px; 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 { declare global {

View File

@@ -12,8 +12,8 @@ import {
HassioHostInfo, HassioHostInfo,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { import {
HassioInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
HassioInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult | void { public render(): TemplateResult | void {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
> >
<span slot="header">System</span> <span slot="header">System</span>
<div class="content"> <div class="content">
<h1>Information</h1>
<div class="card-group"> <div class="card-group">
<hassio-supervisor-info <hassio-supervisor-info
.hass=${this.hass} .hass=${this.hass}
.hostInfo=${this.hostInfo}
.supervisorInfo=${this.supervisorInfo} .supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info> ></hassio-supervisor-info>
<hassio-host-info <hassio-host-info
@@ -65,6 +65,7 @@ class HassioSystem extends LitElement {
.hassOsInfo=${this.hassOsInfo} .hassOsInfo=${this.hassOsInfo}
></hassio-host-info> ></hassio-host-info>
</div> </div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div> </div>
</hass-tabs-subpage> </hass-tabs-subpage>

View File

@@ -23,29 +23,25 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@formatjs/intl-pluralrules": "^1.5.8", "@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/common": "5.1.0", "@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/core": "5.1.0", "@fullcalendar/daygrid": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "5.1.0", "@material/chips": "=8.0.0-canary.a78ceb112.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/circular-progress": "=8.0.0-canary.a78ceb112.0",
"@material/mwc-button": "^0.18.0", "@material/mwc-button": "^0.17.2",
"@material/mwc-checkbox": "^0.18.0", "@material/mwc-checkbox": "^0.17.2",
"@material/mwc-dialog": "^0.18.0", "@material/mwc-dialog": "^0.17.2",
"@material/mwc-fab": "^0.18.0", "@material/mwc-fab": "^0.17.2",
"@material/mwc-formfield": "^0.18.0", "@material/mwc-formfield": "^0.17.2",
"@material/mwc-icon-button": "^0.18.0", "@material/mwc-icon-button": "^0.17.2",
"@material/mwc-list": "^0.18.0", "@material/mwc-list": "^0.17.2",
"@material/mwc-menu": "^0.18.0", "@material/mwc-menu": "^0.17.2",
"@material/mwc-radio": "^0.18.0", "@material/mwc-ripple": "^0.17.2",
"@material/mwc-ripple": "^0.18.0", "@material/mwc-switch": "^0.17.2",
"@material/mwc-switch": "^0.18.0", "@material/mwc-tab": "^0.17.2",
"@material/mwc-tab": "^0.18.0", "@material/mwc-tab-bar": "^0.17.2",
"@material/mwc-tab-bar": "^0.18.0", "@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0", "@mdi/js": "5.3.45",
"@mdi/js": "5.5.55", "@mdi/svg": "5.3.45",
"@mdi/svg": "5.5.55",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2", "@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2", "@polymer/app-storage": "^3.0.2",
@@ -79,7 +75,6 @@
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0", "@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
@@ -89,7 +84,6 @@
"codemirror": "^5.49.0", "codemirror": "^5.49.0",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
@@ -106,16 +100,14 @@
"lit-element": "^2.3.1", "lit-element": "^2.3.1",
"lit-html": "^1.2.1", "lit-html": "^1.2.1",
"lit-virtualizer": "^0.4.2", "lit-virtualizer": "^0.4.2",
"marked": "^1.1.1", "marked": "^0.6.1",
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2", "memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5", "node-vibrant": "^3.1.5",
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12", "superstruct": "^0.10.12",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vue": "^2.6.11", "vue": "^2.6.11",
@@ -144,14 +136,13 @@
"@rollup/plugin-replace": "^2.3.2", "@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-receiver": "^3.0.12",
"@types/codemirror": "^0.0.97", "@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3", "@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1", "@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.1.0",
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2", "@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3", "@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0", "@typescript-eslint/eslint-plugin": "^2.28.0",
@@ -162,7 +153,7 @@
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1", "eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1", "eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2", "eslint-import-resolver-webpack": "^0.12.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0", "eslint-plugin-lit": "^1.2.0",
@@ -185,7 +176,7 @@
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"map-stream": "^0.0.7", "map-stream": "^0.0.7",
"merge-stream": "^1.0.1", "merge-stream": "^1.0.1",
"mocha": "^7.2.0", "mocha": "^6.0.2",
"object-hash": "^2.0.3", "object-hash": "^2.0.3",
"open": "^7.0.4", "open": "^7.0.4",
"prettier": "^2.0.4", "prettier": "^2.0.4",
@@ -202,7 +193,7 @@
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6", "terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.0", "ts-lit-plugin": "^1.2.0",
"ts-mocha": "^7.0.0", "ts-mocha": "^6.0.0",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
@@ -219,11 +210,7 @@
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"lit-html": "1.2.1", "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", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20200912.0", version="20200716.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@@ -16,7 +16,6 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow"; import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import punycode from "punycode";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider"); import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
@@ -76,7 +75,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
${this.localize( ${this.localize(
"ui.panel.page-authorize.authorizing_client", "ui.panel.page-authorize.authorizing_client",
"clientId", "clientId",
this.clientId ? punycode.toASCII(this.clientId) : this.clientId this.clientId
)} )}
</p> </p>
${loggingInWith} ${loggingInWith}

View File

@@ -1,113 +0,0 @@
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);
};

View File

@@ -1,16 +0,0 @@
// 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);
};

View File

@@ -1,24 +0,0 @@
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);
};

View File

@@ -1,9 +0,0 @@
import { HomeAssistant } from "../../types";
/** Return an array of domains with the service. */
export const componentsWithService = (
hass: HomeAssistant,
service: string
): Array<string> =>
hass &&
Object.keys(hass.services).filter((key) => service in hass.services[key]);

View File

@@ -1,9 +0,0 @@
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];

View File

@@ -44,6 +44,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"script", "script",
"sun", "sun",
"timer", "timer",
"updater",
"vacuum", "vacuum",
"water_heater", "water_heater",
"weather", "weather",

View File

@@ -1,4 +1,10 @@
export default function durationToSeconds(duration: string): number { export default function durationToSeconds(duration: string): number {
let days = 0;
if (duration.includes("day")) {
const position = duration.indexOf("day");
days = Number(duration.substr(0, position));
duration = duration.split(",")[1];
}
const parts = duration.split(":").map(Number); const parts = duration.split(":").map(Number);
return parts[0] * 3600 + parts[1] * 60 + parts[2]; return (days * 24 + parts[0]) * 3600 + parts[1] * 60 + parts[2];
} }

View File

@@ -21,11 +21,6 @@ export default function relativeTime(
const tense = delta >= 0 ? "past" : "future"; const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta); delta = Math.abs(delta);
let roundedDelta = Math.round(delta); let roundedDelta = Math.round(delta);
if (roundedDelta === 0) {
return localize("ui.components.relative_time.just_now");
}
let unit = "week"; let unit = "week";
for (let i = 0; i < tests.length; i++) { for (let i = 0; i < tests.length; i++) {

View File

@@ -1,155 +0,0 @@
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,
});
}
},
};
};
};

View File

@@ -1,20 +1,26 @@
import { derivedStyles, darkStyles } from "../../resources/styles"; import { derivedStyles } from "../../resources/styles";
import { HomeAssistant, Theme } from "../../types"; 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 { interface ProcessedTheme {
keys: { [key: string]: "" }; keys: { [key: string]: "" };
styles: { [key: string]: 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 } = {}; let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
/** /**
@@ -27,56 +33,17 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
export const applyThemesOnElement = ( export const applyThemesOnElement = (
element, element,
themes: HomeAssistant["themes"], themes: HomeAssistant["themes"],
selectedTheme?: string, selectedTheme?: string
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
) => { ) => {
let cacheKey = selectedTheme; const newTheme = selectedTheme
let themeRules: Partial<Theme> = {}; ? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
: undefined;
if (selectedTheme === "default" && themeOptions) { if (!element._themes && !newTheme) {
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 // No styles to reset, and no styles to set
return; return;
} }
const newTheme =
themeRules && cacheKey
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
: undefined;
// Add previous set keys to reset them, and new theme // Add previous set keys to reset them, and new theme
const styles = { ...element._themes, ...newTheme?.styles }; const styles = { ...element._themes, ...newTheme?.styles };
element._themes = newTheme?.keys; element._themes = newTheme?.keys;
@@ -91,45 +58,42 @@ export const applyThemesOnElement = (
}; };
const processTheme = ( const processTheme = (
cacheKey: string, themeName: string,
theme: Partial<Theme> themes: HomeAssistant["themes"]
): ProcessedTheme | undefined => { ): ProcessedTheme | undefined => {
if (!theme || !Object.keys(theme).length) { if (!themes.themes[themeName]) {
return undefined; return undefined;
} }
const combinedTheme: Partial<Theme> = { const theme: Theme = {
...derivedStyles, ...derivedStyles,
...theme, ...themes.themes[themeName],
}; };
const styles = {}; const styles = {};
const keys = {}; const keys = {};
for (const key of Object.keys(combinedTheme)) { for (const key of Object.keys(theme)) {
const prefixedKey = `--${key}`; const prefixedKey = `--${key}`;
const value = String(combinedTheme[key]!); const value = theme[key];
styles[prefixedKey] = value; styles[prefixedKey] = value;
keys[prefixedKey] = ""; keys[prefixedKey] = "";
// Try to create a rgb value for this key if it is not a var // Try to create a rgb value for this key if it is a hex color
if (value.startsWith("#")) { if (!value.startsWith("#")) {
// Can't convert non hex value // Not a hex color
continue; continue;
} }
const rgbKey = `rgb-${key}`; const rgbKey = `rgb-${key}`;
if (combinedTheme[rgbKey] !== undefined) { if (theme[rgbKey] !== undefined) {
// Theme has it's own rgb value // Theme has it's own rgb value
continue; continue;
} }
try { const rgbValue = hexToRgb(value);
const rgbValue = hex2rgb(value).join(","); if (rgbValue !== null) {
const prefixedRgbKey = `--${rgbKey}`; const prefixedRgbKey = `--${rgbKey}`;
styles[prefixedRgbKey] = rgbValue; styles[prefixedRgbKey] = rgbValue;
keys[prefixedRgbKey] = ""; keys[prefixedRgbKey] = "";
} catch (e) {
continue;
} }
} }
PROCESSED_THEMES[cacheKey] = { styles, keys }; PROCESSED_THEMES[themeName] = { styles, keys };
return { styles, keys }; return { styles, keys };
}; };

View File

@@ -22,6 +22,9 @@ const _load = (
(element as HTMLScriptElement).async = true; (element as HTMLScriptElement).async = true;
if (type) { if (type) {
(element as HTMLScriptElement).type = 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; break;
case "link": case "link":

View File

@@ -1,4 +1,4 @@
import type { Map, TileLayer } from "leaflet"; import type { Map } from "leaflet";
// Sets up a Leaflet map on the provided DOM element // Sets up a Leaflet map on the provided DOM element
export type LeafletModuleType = typeof import("leaflet"); export type LeafletModuleType = typeof import("leaflet");
@@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement,
darkMode?: boolean, darkMode = false,
draw = false draw = false
): Promise<[Map, LeafletModuleType, TileLayer]> => { ): Promise<[Map, LeafletModuleType]> => {
if (!mapElement.parentNode) { if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
} }
@@ -28,28 +28,15 @@ export const setupLeafletMap = async (
style.setAttribute("rel", "stylesheet"); style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style); mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13); map.setView([52.3731339, 4.8903147], 13);
createTileLayer(Leaflet, darkMode).addTo(map);
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map); return [map, Leaflet];
return [map, Leaflet, tileLayer];
}; };
export const replaceTileLayer = ( export const createTileLayer = (
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, leaflet: LeafletModuleType,
darkMode: boolean darkMode: boolean
): TileLayer => { ) => {
return leaflet.tileLayer( return leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${ `https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all" darkMode ? "dark_all" : "light_all"

View File

@@ -13,7 +13,7 @@ export const batteryIcon = (
return "hass:battery-unknown"; return "hass:battery-unknown";
} }
let icon = "hass:battery"; var icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10; const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) { if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`; icon += `-charging-${batteryRound}`;

View File

@@ -3,51 +3,49 @@ import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
export const binarySensorIcon = (state: HassEntity) => { export const binarySensorIcon = (state: HassEntity) => {
const is_off = state.state && state.state === "off"; const activated = state.state && state.state === "off";
switch (state.attributes.device_class) { switch (state.attributes.device_class) {
case "battery": case "battery":
return is_off ? "hass:battery" : "hass:battery-outline"; return activated ? "hass:battery" : "hass:battery-outline";
case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging";
case "cold": case "cold":
return is_off ? "hass:thermometer" : "hass:snowflake"; return activated ? "hass:thermometer" : "hass:snowflake";
case "connectivity": case "connectivity":
return is_off ? "hass:server-network-off" : "hass:server-network"; return activated ? "hass:server-network-off" : "hass:server-network";
case "door": case "door":
return is_off ? "hass:door-closed" : "hass:door-open"; return activated ? "hass:door-closed" : "hass:door-open";
case "garage_door": case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open"; return activated ? "hass:garage" : "hass:garage-open";
case "gas": case "gas":
case "power": case "power":
case "problem": case "problem":
case "safety": case "safety":
case "smoke": case "smoke":
return is_off ? "hass:shield-check" : "hass:alert"; return activated ? "hass:shield-check" : "hass:alert";
case "heat": case "heat":
return is_off ? "hass:thermometer" : "hass:fire"; return activated ? "hass:thermometer" : "hass:fire";
case "light": case "light":
return is_off ? "hass:brightness-5" : "hass:brightness-7"; return activated ? "hass:brightness-5" : "hass:brightness-7";
case "lock": case "lock":
return is_off ? "hass:lock" : "hass:lock-open"; return activated ? "hass:lock" : "hass:lock-open";
case "moisture": case "moisture":
return is_off ? "hass:water-off" : "hass:water"; return activated ? "hass:water-off" : "hass:water";
case "motion": case "motion":
return is_off ? "hass:walk" : "hass:run"; return activated ? "hass:walk" : "hass:run";
case "occupancy": case "occupancy":
return is_off ? "hass:home-outline" : "hass:home"; return activated ? "hass:home-outline" : "hass:home";
case "opening": case "opening":
return is_off ? "hass:square" : "hass:square-outline"; return activated ? "hass:square" : "hass:square-outline";
case "plug": case "plug":
return is_off ? "hass:power-plug-off" : "hass:power-plug"; return activated ? "hass:power-plug-off" : "hass:power-plug";
case "presence": case "presence":
return is_off ? "hass:home-outline" : "hass:home"; return activated ? "hass:home-outline" : "hass:home";
case "sound": case "sound":
return is_off ? "hass:music-note-off" : "hass:music-note"; return activated ? "hass:music-note-off" : "hass:music-note";
case "vibration": case "vibration":
return is_off ? "hass:crop-portrait" : "hass:vibrate"; return activated ? "hass:crop-portrait" : "hass:vibrate";
case "window": case "window":
return is_off ? "hass:window-closed" : "hass:window-open"; return activated ? "hass:window-closed" : "hass:window-open";
default: default:
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
} }
}; };

View File

@@ -5,16 +5,12 @@ import { domainIcon } from "./domain_icon";
import { batteryIcon } from "./battery_icon"; import { batteryIcon } from "./battery_icon";
const fixedDeviceClassIcons = { const fixedDeviceClassIcons = {
current: "hass:current-ac",
energy: "hass:flash",
humidity: "hass:water-percent", humidity: "hass:water-percent",
illuminance: "hass:brightness-5", illuminance: "hass:brightness-5",
temperature: "hass:thermometer", temperature: "hass:thermometer",
pressure: "hass:gauge", pressure: "hass:gauge",
power: "hass:flash", power: "hass:flash",
power_factor: "hass:angle-acute",
signal_strength: "hass:wifi", signal_strength: "hass:wifi",
voltage: "hass:sine-wave",
}; };
export const sensorIcon = (state: HassEntity) => { export const sensorIcon = (state: HassEntity) => {

View File

@@ -3,10 +3,9 @@ import { HomeAssistant } from "../../types";
import { DOMAINS_WITH_CARD } from "../const"; import { DOMAINS_WITH_CARD } from "../const";
import { canToggleState } from "./can_toggle_state"; import { canToggleState } from "./can_toggle_state";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE } from "../../data/entity";
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => { export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === "unavailable") {
return "display"; return "display";
} }

View File

@@ -4,6 +4,6 @@ export const supportsFeature = (
stateObj: HassEntity, stateObj: HassEntity,
feature: number feature: number
): boolean => { ): boolean => {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line:no-bitwise
return (stateObj.attributes.supported_features! & feature) !== 0; return (stateObj.attributes.supported_features! & feature) !== 0;
}; };

View File

@@ -1,12 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import durationToSeconds from "../datetime/duration_to_seconds"; import durationToSeconds from "../datetime/duration_to_seconds";
export const timerTimeRemaining = ( export const timerTimeRemaining = (stateObj: HassEntity) => {
stateObj: HassEntity
): undefined | number => {
if (!stateObj.attributes.remaining) {
return undefined;
}
let timeRemaining = durationToSeconds(stateObj.attributes.remaining); let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
if (stateObj.state === "active") { if (stateObj.state === "active") {

View File

@@ -2,3 +2,11 @@ const validEntityId = /^(\w+)\.(\w+)$/;
export const isValidEntityId = (entityId: string) => export const isValidEntityId = (entityId: string) =>
validEntityId.test(entityId); 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

View File

@@ -1,14 +1,14 @@
import { import {
ListItem,
RequestSelectedDetail, RequestSelectedDetail,
ListItem,
} from "@material/mwc-list/mwc-list-item"; } from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = ( export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent<RequestSelectedDetail> ev: CustomEvent<RequestSelectedDetail>
): boolean => { ): boolean => {
if (!ev.detail.selected || ev.detail.source !== "property") { if (!ev.detail.selected && ev.detail.source !== "property") {
return false; return false;
} }
(ev.currentTarget as ListItem).selected = false; (ev.target as ListItem).selected = false;
return true; return true;
}; };

View File

@@ -1,5 +1,5 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 // https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => { export const slugify = (value: string, delimiter = "-") => {
const a = const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;"; "àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`; const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;

View File

@@ -1,50 +0,0 @@
// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto.
export const throttle = <T extends Function>(
func: T,
wait: number,
leading = true,
trailing = true
): T => {
let timeout: number | undefined;
let previous = 0;
let context: any;
let args: any;
const later = () => {
previous = leading === false ? 0 : Date.now();
timeout = undefined;
func.apply(context, args);
if (!timeout) {
context = null;
args = null;
}
};
// @ts-ignore
return function (...argmnts) {
// @ts-ignore
// @typescript-eslint/no-this-alias
context = this;
args = argmnts;
const now = Date.now();
if (!previous && leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
previous = now;
func.apply(context, args);
} else if (!timeout && trailing !== false) {
timeout = window.setTimeout(later, remaining);
}
};
};

View File

@@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
callService() { callService() {
this.progress = true; this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this; var el = this;
const eventData = { var eventData = {
domain: this.domain, domain: this.domain,
service: this.service, service: this.service,
serviceData: this.serviceData, serviceData: this.serviceData,

View File

@@ -0,0 +1,110 @@
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);

View File

@@ -1,114 +0,0 @@
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;
}
}

View File

@@ -3,21 +3,19 @@ import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
eventOptions,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
query, query,
TemplateResult, TemplateResult,
eventOptions,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { scroll } from "lit-virtualizer"; 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 { fireEvent } from "../../common/dom/fire_event";
import "../../common/search/search-input"; import "../../common/search/search-input";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
@@ -26,6 +24,8 @@ import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox";
import "../ha-icon"; import "../ha-icon";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
declare global { declare global {
// for fire event // for fire event
@@ -70,7 +70,6 @@ export interface DataTableColumnData extends DataTableSortColumnData {
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;
forceLTR?: boolean; forceLTR?: boolean;
hidden?: boolean;
} }
export interface DataTableRowData { export interface DataTableRowData {
@@ -215,15 +214,13 @@ export class HaDataTable extends LitElement {
class="mdc-data-table__table ${classMap({ class="mdc-data-table__table ${classMap({
"auto-height": this.autoHeight, "auto-height": this.autoHeight,
})}" })}"
role="table"
aria-rowcount=${this._filteredData.length}
style=${styleMap({ style=${styleMap({
height: this.autoHeight height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px` ? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._header?.clientHeight}px)`, : `calc(100% - ${this._header?.clientHeight}px)`,
})} })}
> >
<div class="mdc-data-table__header-row" role="row"> <div class="mdc-data-table__header-row">
${this.selectable ${this.selectable
? html` ? html`
<div <div
@@ -243,10 +240,8 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(this.columns).map(([key, column]) => { ${Object.entries(this.columns).map((columnEntry) => {
if (column.hidden) { const [key, column] = columnEntry;
return "";
}
const sorted = key === this._sortColumn; const sorted = key === this._sortColumn;
const classes = { const classes = {
"mdc-data-table__header-cell--numeric": Boolean( "mdc-data-table__header-cell--numeric": Boolean(
@@ -293,8 +288,8 @@ export class HaDataTable extends LitElement {
${!this._filteredData.length ${!this._filteredData.length
? html` ? html`
<div class="mdc-data-table__content"> <div class="mdc-data-table__content">
<div class="mdc-data-table__row" role="row"> <div class="mdc-data-table__row">
<div class="mdc-data-table__cell grows center" role="cell"> <div class="mdc-data-table__cell grows center">
${this.noDataText || "No data"} ${this.noDataText || "No data"}
</div> </div>
</div> </div>
@@ -309,14 +304,12 @@ export class HaDataTable extends LitElement {
items: !this.hasFab items: !this.hasFab
? this._filteredData ? this._filteredData
: [...this._filteredData, ...[{ empty: true }]], : [...this._filteredData, ...[{ empty: true }]],
renderItem: (row: DataTableRowData, index) => { renderItem: (row: DataTableRowData) => {
if (row.empty) { if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `; return html` <div class="mdc-data-table__row"></div> `;
} }
return html` return html`
<div <div
aria-rowindex=${index}
role="row"
.rowId="${row[this.id]}" .rowId="${row[this.id]}"
@click=${this._handleRowClick} @click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({ class="mdc-data-table__row ${classMap({
@@ -335,7 +328,6 @@ export class HaDataTable extends LitElement {
? html` ? html`
<div <div
class="mdc-data-table__cell mdc-data-table__cell--checkbox" class="mdc-data-table__cell mdc-data-table__cell--checkbox"
role="cell"
> >
<ha-checkbox <ha-checkbox
class="mdc-data-table__row-checkbox" class="mdc-data-table__row-checkbox"
@@ -349,14 +341,10 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(this.columns).map( ${Object.entries(this.columns).map((columnEntry) => {
([key, column]) => { const [key, column] = columnEntry;
if (column.hidden) {
return "";
}
return html` return html`
<div <div
role="cell"
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean( "mdc-data-table__cell--numeric": Boolean(
column.type === "numeric" column.type === "numeric"
@@ -386,8 +374,7 @@ export class HaDataTable extends LitElement {
: row[key]} : row[key]}
</div> </div>
`; `;
} })}
)}
</div> </div>
`; `;
}, },
@@ -554,7 +541,7 @@ export class HaDataTable extends LitElement {
border-radius: 4px; border-radius: 4px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: var(--divider-color); border-color: rgba(var(--rgb-primary-text-color), 0.12);
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
@@ -572,7 +559,7 @@ export class HaDataTable extends LitElement {
} }
.mdc-data-table__row ~ .mdc-data-table__row { .mdc-data-table__row ~ .mdc-data-table__row {
border-top: 1px solid var(--divider-color); border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
} }
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover { .mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
@@ -591,7 +578,7 @@ export class HaDataTable extends LitElement {
height: 56px; height: 56px;
display: flex; display: flex;
width: 100%; width: 100%;
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
overflow-x: auto; overflow-x: auto;
} }
@@ -844,7 +831,7 @@ export class HaDataTable extends LitElement {
right: 12px; right: 12px;
} }
.table-header { .table-header {
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
padding: 0 16px; padding: 0 16px;
} }
search-input { search-input {

View File

@@ -1,11 +1,11 @@
// To use comlink under ES5 // To use comlink under ES5
import { expose } from "comlink";
import "proxy-polyfill"; import "proxy-polyfill";
import { expose } from "comlink";
import type { import type {
DataTableRowData,
DataTableSortColumnData, DataTableSortColumnData,
SortableColumnContainer, DataTableRowData,
SortingDirection, SortingDirection,
SortableColumnContainer,
} from "./ha-data-table"; } from "./ha-data-table";
const filterData = ( const filterData = (
@@ -19,7 +19,7 @@ const filterData = (
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
if ( if (
String(column.filterKey ? row[key][column.filterKey] : row[key]) (column.filterKey ? row[key][column.filterKey] : row[key])
.toUpperCase() .toUpperCase()
.includes(filter) .includes(filter)
) { ) {

View File

@@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
} }
.daterangepicker td.in-range { .daterangepicker td.in-range {
background-color: var(--light-primary-color); background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color)); color: var(--primary-text-color);
} }
.daterangepicker td.active, .daterangepicker td.active,
.daterangepicker td.active:hover { .daterangepicker td.active:hover {

View File

@@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
* @return {!Array<!HTMLElement>} * @return {!Array<!HTMLElement>}
*/ */
getTabbableNodes: function (node) { getTabbableNodes: function (node) {
const result = []; var result = [];
// If there is at least one element with tabindex > 0, we need to sort // If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex. // the final array by tabindex.
const needsSortByTabIndex = this._collectTabbableNodes(node, result); var needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) { if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result); return IronFocusablesHelper._sortByTabIndex(result);
} }
@@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
) { ) {
return false; return false;
} }
const element = /** @type {!HTMLElement} */ (node); var element = /** @type {!HTMLElement} */ (node);
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element); var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
let needsSort = tabIndex > 0; var needsSort = tabIndex > 0;
if (tabIndex >= 0) { if (tabIndex >= 0) {
result.push(element); result.push(element);
} }
@@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
// <input id="B" slot="b" tabindex="1"> // <input id="B" slot="b" tabindex="1">
// </div> // </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0. // TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
let children; var children;
if (element.localName === "content" || element.localName === "slot") { if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes(); children = dom(element).getDistributedNodes();
} else { } else {
@@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
children = dom(element.shadowRoot || element.root || element).children; children = dom(element.shadowRoot || element.root || element).children;
// ///////////////////////// // /////////////////////////
} }
for (let i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children. // Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort; needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
} }

View File

@@ -1,12 +1,12 @@
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
import "../ha-icon-button";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { timeOut } from "@polymer/polymer/lib/utils/async"; import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { formatTime } from "../../common/datetime/format_time"; import { formatTime } from "../../common/datetime/format_time";
import "../ha-icon-button";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
/* global Chart moment Color */ /* global Chart moment Color */
@@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors(
return value; return value;
} }
const date = new Date(values[index].value); const date = new Date(values[index].value);
return formatTime(date, this.hass.language); return formatTime(date);
} }
drawChart() { drawChart() {
@@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors(
}, },
}; };
options = Chart.helpers.merge(options, this.data.options); options = Chart.helpers.merge(options, this.data.options);
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this); options.scales.xAxes[0].ticks.callback = this._formatTickValue;
if (this.data.type === "timeline") { if (this.data.type === "timeline") {
this.set("isTimeline", true); this.set("isTimeline", true);
if (this.data.colors !== undefined) { if (this.data.colors !== undefined) {

View File

@@ -1,178 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px;
padding: 0;
}
</style>
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property() public value?: string;
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
: [];
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<vaadin-combo-box-light
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-attribute-picker": HaEntityAttributePicker;
}
}

View File

@@ -1,3 +1,4 @@
import "../ha-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -6,7 +7,6 @@ import { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
customElement,
html, html,
LitElement, LitElement,
property, property,
@@ -20,7 +20,6 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge"; import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -52,8 +51,7 @@ const rowRenderer = (
root.querySelector("[secondary]")!.textContent = model.item.entity_id; root.querySelector("[secondary]")!.textContent = model.item.entity_id;
}; };
@customElement("ha-entity-picker") class HaEntityPicker extends LitElement {
export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@@ -97,8 +95,6 @@ export class HaEntityPicker extends LitElement {
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement; @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
private _initedStates = false;
private _getStates = memoizeOne( private _getStates = memoizeOne(
( (
_opened: boolean, _opened: boolean,
@@ -152,18 +148,11 @@ export class HaEntityPicker extends LitElement {
); );
protected shouldUpdate(changedProps: PropertyValues) { protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { if (changedProps.has("_opened") && this._opened) {
const states = this._getStates( const states = this._getStates(
this._opened, this._opened,
this.hass, this.hass,
@@ -173,7 +162,6 @@ export class HaEntityPicker extends LitElement {
this.includeDeviceClasses this.includeDeviceClasses
); );
(this._comboBox as any).items = states; (this._comboBox as any).items = states;
this._initedStates = true;
} }
} }
@@ -181,6 +169,7 @@ export class HaEntityPicker extends LitElement {
if (!this.hass) { if (!this.hass) {
return html``; return html``;
} }
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="entity_id" item-value-path="entity_id"
@@ -278,6 +267,8 @@ export class HaEntityPicker extends LitElement {
} }
} }
customElements.define("ha-entity-picker", HaEntityPicker);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-entity-picker": HaEntityPicker; "ha-entity-picker": HaEntityPicker;

View File

@@ -20,7 +20,6 @@ import { stateIcon } from "../../common/entity/state_icon";
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining"; import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
@customElement("ha-state-label-badge") @customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement { export class HaStateLabelBadge extends LitElement {
@@ -82,8 +81,7 @@ export class HaStateLabelBadge extends LitElement {
? "" ? ""
: this.image : this.image
? this.image ? this.image
: state.attributes.entity_picture_local || : state.attributes.entity_picture}"
state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}" .label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${this.name ? this.name : computeStateName(state)}" .description="${this.name ? this.name : computeStateName(state)}"
></ha-label-badge> ></ha-label-badge>
@@ -110,7 +108,7 @@ export class HaStateLabelBadge extends LitElement {
return null; return null;
case "sensor": case "sensor":
default: default:
return state.state === UNKNOWN return state.state === "unknown"
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? state.state ? state.state
@@ -123,7 +121,7 @@ export class HaStateLabelBadge extends LitElement {
} }
private _computeIcon(domain: string, state: HassEntity) { private _computeIcon(domain: string, state: HassEntity) {
if (state.state === UNAVAILABLE) { if (state.state === "unavailable") {
return null; return null;
} }
switch (domain) { switch (domain) {
@@ -168,7 +166,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeLabel(domain, state, _timerTimeRemaining) { private _computeLabel(domain, state, _timerTimeRemaining) {
if ( if (
state.state === UNAVAILABLE || state.state === "unavailable" ||
["device_tracker", "alarm_control_panel", "person"].includes(domain) ["device_tracker", "alarm_control_panel", "person"].includes(domain)
) { ) {
// Localize the state with a special state_badge namespace, which has variations of // Localize the state with a special state_badge namespace, which has variations of

View File

@@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
if (stateObj) { if (stateObj) {
// hide icon if we have entity picture // hide icon if we have entity picture
if ( if (
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) || (stateObj.attributes.entity_picture && !this.overrideIcon) ||
this.overrideImage this.overrideImage
) { ) {
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture; let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
if (this.hass) { if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl); imageUrl = this.hass.hassUrl(imageUrl);
} }

View File

@@ -1,67 +0,0 @@
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;
}
}

View File

@@ -1,20 +1,21 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { import {
css,
CSSResult,
customElement, customElement,
html, html,
LitElement,
property,
TemplateResult, TemplateResult,
property,
LitElement,
CSSResult,
css,
} from "lit-element"; } from "lit-element";
import "./ha-icon-button";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon";
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@property({ attribute: false }) public buttons!: ToggleButton[]; @property() public buttons!: ToggleButton[];
@property() public active?: string; @property() public active?: string;
@@ -22,23 +23,21 @@ export class HaButtonToggleGroup extends LitElement {
return html` return html`
<div> <div>
${this.buttons.map( ${this.buttons.map(
(button) => html` (button) => html` <ha-icon-button
<mwc-icon-button
.label=${button.label} .label=${button.label}
.icon=${button.icon}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}
> >
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon> </ha-icon-button>`
</mwc-icon-button>
`
)} )}
</div> </div>
`; `;
} }
private _handleClick(ev): void { private _handleClick(ev): void {
this.active = ev.currentTarget.value; this.active = ev.target.value;
fireEvent(this, "value-changed", { value: this.active }); fireEvent(this, "value-changed", { value: this.active });
} }
@@ -49,13 +48,12 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px); --mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px); --mdc-icon-size: var(--button-toggle-icon-size, 20px);
} }
mwc-icon-button { ha-icon-button {
border: 1px solid var(--primary-color); border: 1px solid var(--primary-color);
border-right-width: 0px; border-right-width: 0px;
position: relative; position: relative;
cursor: pointer;
} }
mwc-icon-button::before { ha-icon-button::before {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@@ -67,26 +65,22 @@ export class HaButtonToggleGroup extends LitElement {
content: ""; content: "";
transition: opacity 15ms linear, background-color 15ms linear; transition: opacity 15ms linear, background-color 15ms linear;
} }
mwc-icon-button[active]::before { ha-icon-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12); opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
} }
mwc-icon-button:first-child { ha-icon-button:first-child {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
mwc-icon-button:last-child { ha-icon-button:last-child {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
border-right-width: 1px; border-right-width: 1px;
} }
mwc-icon-button:only-child {
border-radius: 4px;
border-right-width: 1px;
}
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-button-toggle-group": HaButtonToggleGroup; "ha-button-toggle-button": HaButtonToggleGroup;
} }
} }

View File

@@ -3,9 +3,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
@@ -18,28 +18,37 @@ import {
fetchStreamUrl, fetchStreamUrl,
} from "../data/camera"; } from "../data/camera";
import { CameraEntity, HomeAssistant } from "../types"; import { CameraEntity, HomeAssistant } from "../types";
import "./ha-hls-player";
type HLSModule = typeof import("hls.js");
@customElement("ha-camera-stream") @customElement("ha-camera-stream")
class HaCameraStream extends LitElement { class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: CameraEntity; @property() public stateObj?: CameraEntity;
@property({ type: Boolean, attribute: "controls" }) @property({ type: Boolean }) public showControls = false;
public controls = false;
@property({ type: Boolean, attribute: "muted" }) @internalProperty() private _attached = false;
public muted = false;
// We keep track if we should force MJPEG with a string // We keep track if we should force MJPEG with a string
// that way it automatically resets if we change entity. // that way it automatically resets if we change entity.
@internalProperty() private _forceMJPEG?: string; @internalProperty() private _forceMJPEG: string | undefined = undefined;
@internalProperty() private _url?: string; private _hlsPolyfillInstance?: Hls;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.stateObj) { if (!this.stateObj || !this._attached) {
return html``; return html``;
} }
@@ -56,25 +65,51 @@ class HaCameraStream extends LitElement {
)} camera.`} )} camera.`}
/> />
` `
: this._url : html`
? html` <video
<ha-hls-player
autoplay autoplay
muted
playsinline playsinline
.muted=${this.muted} ?controls=${this.showControls}
.controls=${this.controls} @loadeddata=${this._elementResized}
.hass=${this.hass} ></video>
.url=${this._url} `}
></ha-hls-player>
`
: ""}
`; `;
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues) {
if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) { super.updated(changedProps);
this._forceMJPEG = undefined;
this._getStreamUrl(); 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();
} }
} }
@@ -86,35 +121,96 @@ class HaCameraStream extends LitElement {
); );
} }
private async _getStreamUrl(): Promise<void> { 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;
}
try { try {
const { url } = await fetchStreamUrl( const { url } = await fetchStreamUrl(
this.hass!, this.hass!,
this.stateObj!.entity_id this.stateObj!.entity_id
); );
this._url = url; if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) { } catch (err) {
// Fails if we were unable to get a stream // Fails if we were unable to get a stream
// eslint-disable-next-line // eslint-disable-next-line
console.error(err); console.error(err);
this._forceMJPEG = this.stateObj!.entity_id; 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() { private _elementResized() {
fireEvent(this, "iron-resize"); fireEvent(this, "iron-resize");
} }
private _destroyPolyfill(): void {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host, :host,
img { img,
video {
display: block; display: block;
} }
img { img,
video {
width: 100%; width: 100%;
} }
`; `;

View File

@@ -66,7 +66,7 @@ export class HaCard extends LitElement {
} }
:host ::slotted(.card-actions) { :host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8); border-top: 1px solid #e8e8e8;
padding: 5px 16px; padding: 5px 16px;
} }
`; `;

View File

@@ -1,16 +1,16 @@
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import { import {
css,
customElement,
html,
LitElement, LitElement,
TemplateResult,
property, property,
svg, svg,
SVGTemplateResult, html,
TemplateResult, customElement,
unsafeCSS, unsafeCSS,
SVGTemplateResult,
css,
} from "lit-element"; } from "lit-element";
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
@customElement("ha-circular-progress") @customElement("ha-circular-progress")
@@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement {
@property() @property()
public size: "small" | "medium" | "large" = "medium"; public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult { protected render(): TemplateResult | void {
let indeterminatePart: SVGTemplateResult; let indeterminatePart: SVGTemplateResult;
if (this.size === "small") { if (this.size === "small") {

View File

@@ -1,8 +1,8 @@
import { Editor } from "codemirror"; import { Editor } from "codemirror";
import { import {
customElement, customElement,
internalProperty,
property, property,
internalProperty,
PropertyValues, PropertyValues,
UpdatingElement, UpdatingElement,
} from "lit-element"; } from "lit-element";
@@ -97,11 +97,15 @@ export class HaCodeEditor extends UpdatingElement {
.CodeMirror { .CodeMirror {
height: var(--code-mirror-height, auto); height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr); direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
} }
.CodeMirror-scroll { .CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height); 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 { :host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red); border-color: var(--error-state-color, red);
} }
@@ -109,7 +113,7 @@ export class HaCodeEditor extends UpdatingElement {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color)); border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
} }
.CodeMirror-linenumber { .CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color)); color: var(--paper-dialog-color, var(--primary-text-color));
} }
.rtl .CodeMirror-vscrollbar { .rtl .CodeMirror-vscrollbar {
right: auto; right: auto;
@@ -118,100 +122,6 @@ export class HaCodeEditor extends UpdatingElement {
.rtl-gutter { .rtl-gutter {
width: 20px; 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>`; </style>`;
this.codemirror = codeMirror(shadowRoot, { this.codemirror = codeMirror(shadowRoot, {

View File

@@ -176,11 +176,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
this.drawColorWheel(); this.drawColorWheel();
this.drawMarker(); this.drawMarker();
if (this.desiredHsColor) {
this.setMarkerOnColor(this.desiredHsColor);
this.applyColorToCanvas(this.desiredHsColor);
}
this.interactionLayer.addEventListener("mousedown", (ev) => this.interactionLayer.addEventListener("mousedown", (ev) =>
this.onMouseDown(ev) this.onMouseDown(ev)
); );
@@ -193,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// origin is wheel center // origin is wheel center
// returns {x: X, y: Y} object // returns {x: X, y: Y} object
convertToCanvasCoordinates(clientX, clientY) { convertToCanvasCoordinates(clientX, clientY) {
const svgPoint = this.interactionLayer.createSVGPoint(); var svgPoint = this.interactionLayer.createSVGPoint();
svgPoint.x = clientX; svgPoint.x = clientX;
svgPoint.y = clientY; svgPoint.y = clientY;
const cc = svgPoint.matrixTransform( var cc = svgPoint.matrixTransform(
this.interactionLayer.getScreenCTM().inverse() this.interactionLayer.getScreenCTM().inverse()
); );
return { x: cc.x, y: cc.y }; return { x: cc.x, y: cc.y };
@@ -230,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Touch events // Touch events
onTouchStart(ev) { onTouchStart(ev) {
const touch = ev.changedTouches[0]; var touch = ev.changedTouches[0];
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY); const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
// return if we're not on the wheel // return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) { if (!this.isInWheel(cc.x, cc.y)) {
@@ -280,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Process user input to color // Process user input to color
processUserSelect(ev) { processUserSelect(ev) {
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
const hs = this.getColor(canvasXY.x, canvasXY.y); var hs = this.getColor(canvasXY.x, canvasXY.y);
this.onColorSelect(hs); this.onColorSelect(hs);
} }
@@ -324,23 +319,17 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color // set marker position to the given color
setMarkerOnColor(hs) { setMarkerOnColor(hs) {
if (!this.marker || !this.tooltip) { var dist = hs.s * this.radius;
return; var theta = ((hs.h - 180) / 180) * Math.PI;
} var markerdX = -dist * Math.cos(theta);
const dist = hs.s * this.radius; var markerdY = -dist * Math.sin(theta);
const theta = ((hs.h - 180) / 180) * Math.PI; var translateString = `translate(${markerdX},${markerdY})`;
const markerdX = -dist * Math.cos(theta);
const markerdY = -dist * Math.sin(theta);
const translateString = `translate(${markerdX},${markerdY})`;
this.marker.setAttribute("transform", translateString); this.marker.setAttribute("transform", translateString);
this.tooltip.setAttribute("transform", translateString); this.tooltip.setAttribute("transform", translateString);
} }
// apply given color to interface elements // apply given color to interface elements
applyColorToCanvas(hs) { applyColorToCanvas(hs) {
if (!this.interactionLayer) {
return;
}
// we're not really converting hs to hsl here, but we keep it cheap // we're not really converting hs to hsl here, but we keep it cheap
// setting the color on the interactionLayer, the svg elements can inherit // setting the color on the interactionLayer, the svg elements can inherit
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
@@ -369,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// get angle (degrees) // get angle (degrees)
getAngle(dX, dY) { getAngle(dX, dY) {
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
return angle; return angle;
} }
@@ -389,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
*/ */
getColor(x, y) { getColor(x, y) {
const hue = this.getAngle(x, y); // degrees, clockwise from right var hue = this.getAngle(x, y); // degrees, clockwise from right
const relativeDistance = this.getDistance(x, y); // edge of radius = 1 var relativeDistance = this.getDistance(x, y); // edge of radius = 1
const sat = Math.min(relativeDistance, 1); // Distance from center var sat = Math.min(relativeDistance, 1); // Distance from center
return { h: hue, s: sat }; return { h: hue, s: sat };
} }
@@ -413,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (this.saturationSegments === 1) { if (this.saturationSegments === 1) {
hs.s = 1; hs.s = 1;
} else { } else {
const segmentSize = 1 / this.saturationSegments; var segmentSize = 1 / this.saturationSegments;
const saturationStep = 1 / (this.saturationSegments - 1); var saturationStep = 1 / (this.saturationSegments - 1);
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep; var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
hs.s = Math.min(calculatedSat, 1); hs.s = Math.min(calculatedSat, 1);
} }
} }
@@ -488,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
hueSegments = hueSegments || 360; // reset 0 segments to 360 hueSegments = hueSegments || 360; // reset 0 segments to 360
const angleStep = 360 / hueSegments; const angleStep = 360 / hueSegments;
const halfAngleStep = angleStep / 2; // center segments on color const halfAngleStep = angleStep / 2; // center segments on color
for (let angle = 0; angle <= 360; angle += angleStep) { for (var angle = 0; angle <= 360; angle += angleStep) {
const startAngle = (angle - halfAngleStep) * (Math.PI / 180); var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180); var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
context.beginPath(); context.beginPath();
context.moveTo(cX, cY); context.moveTo(cX, cY);
context.arc( context.arc(
@@ -503,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
); );
context.closePath(); context.closePath();
// gradient // gradient
const gradient = context.createRadialGradient( var gradient = context.createRadialGradient(
cX, cX,
cY, cY,
0, 0,
@@ -518,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (saturationSegments > 0) { if (saturationSegments > 0) {
const ratioStep = 1 / saturationSegments; const ratioStep = 1 / saturationSegments;
let ratio = 0; let ratio = 0;
for (let stop = 1; stop < saturationSegments; stop += 1) { for (var stop = 1; stop < saturationSegments; stop += 1) {
const prevLighness = lightness; var prevLighness = lightness;
ratio = stop * ratioStep; ratio = stop * ratioStep;
lightness = 100 - 50 * ratio; lightness = 100 - 50 * ratio;
gradient.addColorStop( gradient.addColorStop(

View File

@@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
const assumedState = stateObj.attributes.assumed_state === true; var assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState; return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
} }
@@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
const assumedState = stateObj.attributes.assumed_state === true; var assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState; return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
} }

View File

@@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
const assumedState = stateObj.attributes.assumed_state === true; var assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState; return entityObj.isFullyOpenTilt && !assumedState;
} }
@@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
const assumedState = stateObj.attributes.assumed_state === true; var assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState; return entityObj.isFullyClosedTilt && !assumedState;
} }

View File

@@ -163,6 +163,13 @@ export class HaDateRangePicker extends LitElement {
border-right: 1px solid var(--divider-color); 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 { .date-range-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -172,30 +179,12 @@ export class HaDateRangePicker extends LitElement {
paper-input { paper-input {
display: inline-block; display: inline-block;
max-width: 250px; max-width: 200px;
min-width: 200px;
} }
paper-input:last-child { paper-input:last-child {
margin-left: 8px; 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;
}
}
`; `;
} }
} }

View File

@@ -1,16 +1,16 @@
import "@material/mwc-dialog"; import "@material/mwc-dialog";
import type { Dialog } from "@material/mwc-dialog"; import type { Dialog } from "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css"; import { style } from "@material/mwc-dialog/mwc-dialog-css";
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"; import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js";
import { computeRTLDirection } from "../common/util/compute_rtl";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
export const createCloseHeading = (hass: HomeAssistant, title: string) => html` export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<span class="header_title">${title}</span> ${title}
<mwc-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close" dialogAction="close"
@@ -23,10 +23,6 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends MwcDialog { export class HaDialog extends MwcDialog {
public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y);
}
protected renderHeading() { protected renderHeading() {
return html`<slot name="heading"> return html`<slot name="heading">
${super.renderHeading()} ${super.renderHeading()}
@@ -38,12 +34,10 @@ export class HaDialog extends MwcDialog {
style, style,
css` css`
.mdc-dialog { .mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7); z-index: var(--dialog-z-index, 7);
} }
.mdc-dialog__actions { .mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
} }
.mdc-dialog__container { .mdc-dialog__container {
align-items: var(--vertial-align-dialog, center); align-items: var(--vertial-align-dialog, center);
@@ -56,21 +50,10 @@ export class HaDialog extends MwcDialog {
position: var(--dialog-content-position, relative); position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px); 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 { .mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
} }
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button { .header_button {
position: absolute; position: absolute;
right: 16px; right: 16px;
@@ -78,17 +61,10 @@ export class HaDialog extends MwcDialog {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.header_title {
margin-right: 40px;
}
[dir="rtl"].header_button { [dir="rtl"].header_button {
right: auto; right: auto;
left: 16px; left: 16px;
} }
[dir="rtl"].header_title {
margin-left: 40px;
margin-right: 0px;
}
`, `,
]; ];
} }

View File

@@ -54,8 +54,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
` `
: ""} : ""}
<ha-paper-slider <ha-paper-slider
pin pin=""
editable
.value=${this._value} .value=${this._value}
.min=${this.schema.valueMin} .min=${this.schema.valueMin}
.max=${this.schema.valueMax} .max=${this.schema.valueMax}
@@ -112,10 +111,6 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.flex { .flex {
display: flex; display: flex;
} }
ha-paper-slider {
width: 100%;
margin-right: 16px;
}
`; `;
} }
} }

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@@ -11,7 +12,6 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-paper-dropdown-menu";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
@customElement("ha-form-select") @customElement("ha-form-select")
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public suffix!: string; @property() public suffix!: string;
@query("ha-paper-dropdown-menu") private _input?: HTMLElement; @query("paper-dropdown-menu") private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-paper-dropdown-menu .label=${this.label}> <paper-dropdown-menu .label=${this.label}>
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
attr-for-selected="item-value" attr-for-selected="item-value"
@@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
` `
)} )}
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </paper-dropdown-menu>
`; `;
} }
@@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
ha-paper-dropdown-menu { paper-dropdown-menu {
display: block; display: block;
} }
`; `;

View File

@@ -100,7 +100,7 @@ export interface HaFormTimeData {
} }
export interface HaFormElement extends LitElement { export interface HaFormElement extends LitElement {
schema: HaFormSchema | HaFormSchema[]; schema: HaFormSchema;
data?: HaFormDataContainer | HaFormData; data?: HaFormDataContainer | HaFormData;
label?: string; label?: string;
suffix?: string; suffix?: string;
@@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData; @property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema | HaFormSchema[]; @property() public schema!: HaFormSchema;
@property() public error; @property() public error;
@@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""; : "";
} }
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { private _computeError(error, schema: HaFormSchema) {
return this.computeError ? this.computeError(error, schema) : error; return this.computeError ? this.computeError(error, schema) : error;
} }
@@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema; const schema = (ev.target as HaFormElement).schema;
const data = this.data as HaFormDataContainer; const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value; data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {

View File

@@ -9,17 +9,23 @@ import {
} from "lit-element"; } from "lit-element";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { afterNextRender } from "../common/util/render-status"; 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 getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); const percentage = getValueInPercentage(normalize(value, min, max), min, max);
return (percentage * 180) / 100; return (percentage * 180) / 100;
}; };
// Workaround for https://github.com/home-assistant/frontend/issues/6467 const normalize = (value: number, min: number, max: number) => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 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;
};
@customElement("ha-gauge") @customElement("ha-gauge")
export class Gauge extends LitElement { export class Gauge extends LitElement {
@@ -63,28 +69,9 @@ export class Gauge extends LitElement {
></path> ></path>
<path <path
class="value" class="value"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
d="M 90 50.001 A 40 40 0 0 1 10 50" d="M 90 50.001 A 40 40 0 0 1 10 50"
style=${ifDefined( ></path>
!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>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
@@ -119,8 +106,8 @@ export class Gauge extends LitElement {
fill: none; fill: none;
stroke-width: 15; stroke-width: 15;
stroke: var(--gauge-color); stroke: var(--gauge-color);
transition: all 1000ms ease 0s;
transform-origin: 50% 100%; transform-origin: 50% 100%;
transition: all 1s ease 0s;
} }
.gauge { .gauge {
display: block; display: block;

View File

@@ -1,6 +1,6 @@
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
// @ts-ignore // @ts-ignore
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css"; 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") @customElement("ha-header-bar")
export class HaHeaderBar extends LitElement { export class HaHeaderBar extends LitElement {

View File

@@ -1,216 +0,0 @@
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;
}
}

View File

@@ -106,7 +106,6 @@ const mdiRenameMapping = {
pot: "pot-steam", pot: "pot-steam",
ruby: "language-ruby", ruby: "language-ruby",
sailing: "sail-boat", sailing: "sail-boat",
scooter: "human-scooter",
settings: "cog", settings: "cog",
"settings-box": "cog-box", "settings-box": "cog-box",
"settings-outline": "cog-outline", "settings-outline": "cog-outline",
@@ -194,7 +193,6 @@ const mdiRemovedIcons = new Set([
"medium", "medium",
"meetup", "meetup",
"mixcloud", "mixcloud",
"mixer",
"nfc-off", "nfc-off",
"npm-variant", "npm-variant",
"npm-variant-outline", "npm-variant-outline",

View File

@@ -23,6 +23,7 @@ class HaMarkdownElement extends UpdatingElement {
{ {
breaks: this.breaks, breaks: this.breaks,
gfm: true, gfm: true,
tables: true,
}, },
{ {
allowSvg: this.allowSvg, allowSvg: this.allowSvg,

View File

@@ -57,10 +57,6 @@ class HaMarkdown extends LitElement {
background-color: var(--markdown-code-background-color, none); background-color: var(--markdown-code-background-color, none);
border-radius: 3px; 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 { ha-markdown-element code {
font-size: 85%; font-size: 85%;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
@@ -74,8 +70,8 @@ class HaMarkdown extends LitElement {
line-height: 1.45; line-height: 1.45;
} }
ha-markdown-element h2 { ha-markdown-element h2 {
font-size: 1.5em; font-size: 1.5em !important;
font-weight: bold; font-weight: bold !important;
} }
`; `;
} }

View File

@@ -68,10 +68,6 @@ class HaPaperSlider extends PaperSliderClass {
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important; -webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
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); tpl.content.appendChild(styleEl);
return tpl; return tpl;

View File

@@ -1,226 +0,0 @@
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;
}
}

View File

@@ -1,20 +0,0 @@
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;
}
}

View File

@@ -97,7 +97,6 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
</h3> </h3>
<a <a
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`} href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
@click=${this._navigateAwayClose}
> >
${this.hass.localize(`component.${entry.domain}.title`)}: ${this.hass.localize(`component.${entry.domain}.title`)}:
${entry.title} ${entry.title}
@@ -117,10 +116,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3> <h3>
${this.hass.localize("ui.components.related-items.device")}: ${this.hass.localize("ui.components.related-items.device")}:
</h3> </h3>
<a <a href="/config/devices/device/${relatedDeviceId}">
href="/config/devices/device/${relatedDeviceId}"
@click=${this._navigateAwayClose}
>
${device.name_by_user || device.name} ${device.name_by_user || device.name}
</a> </a>
`; `;
@@ -138,10 +134,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3> <h3>
${this.hass.localize("ui.components.related-items.area")}: ${this.hass.localize("ui.components.related-items.area")}:
</h3> </h3>
<a <a href="/config/areas/area/${relatedAreaId}">
href="/config/areas/area/${relatedAreaId}"
@click=${this._navigateAwayClose}
>
${area.name} ${area.name}
</a> </a>
`; `;
@@ -285,12 +278,6 @@ 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() { private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId); this._related = await findRelated(this.hass, this.itemType, this.itemId);
await this.updateComplete; await this.updateComplete;

View File

@@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
} }
_getDescription(hass, domain, service) { _getDescription(hass, domain, service) {
const domainServices = hass.services[domain]; var domainServices = hass.services[domain];
if (!domainServices) return ""; if (!domainServices) return "";
const serviceObject = domainServices[service]; var serviceObject = domainServices[service];
if (!serviceObject) return ""; if (!serviceObject) return "";
return serviceObject.description; return serviceObject.description;
} }

View File

@@ -1,62 +0,0 @@
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
SVGTemplateResult,
} from "lit-element";
@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;
}
::slotted(ha-switch) {
padding: 16px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-settings-row": HaSettingsRow;
}
}

View File

@@ -1,12 +1,9 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { import {
mdiBell, mdiBell,
mdiCellphoneCog, mdiCellphoneCog,
mdiClose,
mdiMenu,
mdiMenuOpen, mdiMenuOpen,
mdiPlus, mdiMenu,
mdiViewDashboard, mdiViewDashboard,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@@ -16,23 +13,20 @@ import "@polymer/paper-listbox/paper-listbox";
import { import {
css, css,
CSSResult, CSSResult,
customElement,
eventOptions, eventOptions,
html, html,
internalProperty, customElement,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; 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 { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { compare } from "../common/string/compare"; import { compare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import { ActionHandlerDetail } from "../data/lovelace"; import { getDefaultPanel } from "../data/panel";
import { import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
@@ -41,8 +35,6 @@ import {
ExternalConfig, ExternalConfig,
getExternalConfig, getExternalConfig,
} from "../external_app/external_config"; } 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 type { HomeAssistant, PanelInfo } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-menu-button"; import "./ha-menu-button";
@@ -62,39 +54,11 @@ const SORT_VALUE_URL_PATHS = {
config: 11, config: 11,
}; };
const panelSorter = ( const panelSorter = (a: PanelInfo, b: PanelInfo) => {
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. // Put all the Lovelace at the top.
const aLovelace = a.component_name === "lovelace"; const aLovelace = a.component_name === "lovelace";
const bLovelace = b.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) { if (aLovelace && bLovelace) {
return compare(a.title!, b.title!); return compare(a.title!, b.title!);
} }
@@ -121,13 +85,8 @@ const defaultPanelSorter = (
return compare(a.title!, b.title!); return compare(a.title!, b.title!);
}; };
const computePanels = memoizeOne( const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
( const panels = hass.panels;
panels: HomeAssistant["panels"],
defaultPanel: HomeAssistant["defaultPanel"],
panelsOrder: string[],
hiddenPanels: string[]
): [PanelInfo[], PanelInfo[]] => {
if (!panels) { if (!panels) {
return [[], []]; return [[], []];
} }
@@ -136,10 +95,7 @@ const computePanels = memoizeOne(
const afterSpacer: PanelInfo[] = []; const afterSpacer: PanelInfo[] = [];
Object.values(panels).forEach((panel) => { Object.values(panels).forEach((panel) => {
if ( if (!panel.title || panel.url_path === hass.defaultPanel) {
hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel)
) {
return; return;
} }
(SHOW_AFTER_SPACER.includes(panel.url_path) (SHOW_AFTER_SPACER.includes(panel.url_path)
@@ -148,24 +104,17 @@ const computePanels = memoizeOne(
).push(panel); ).push(panel);
}); });
const reverseSort = [...panelsOrder].reverse(); beforeSpacer.sort(panelSorter);
afterSpacer.sort(panelSorter);
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
return [beforeSpacer, afterSpacer]; return [beforeSpacer, afterSpacer];
} };
);
let Sortable;
let sortStyles: CSSResult;
@customElement("ha-sidebar") @customElement("ha-sidebar")
class HaSidebar extends LitElement { class HaSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property() public narrow!: boolean;
@property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean }) public alwaysExpand = false;
@@ -175,13 +124,9 @@ class HaSidebar extends LitElement {
@internalProperty() private _notifications?: PersistentNotification[]; @internalProperty() private _notifications?: PersistentNotification[];
@internalProperty() private _editMode = false;
// property used only in css // property used only in css
// @ts-ignore // @ts-ignore
@property({ type: Boolean, reflect: true }) public rtl = false; @property({ type: Boolean, reflect: true }) private _rtl = false;
@internalProperty() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
@@ -189,20 +134,6 @@ class HaSidebar extends LitElement {
private _recentKeydownActiveUntil = 0; 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() { protected render() {
const hass = this.hass; const hass = this.hass;
@@ -210,12 +141,7 @@ class HaSidebar extends LitElement {
return html``; return html``;
} }
const [beforeSpacer, afterSpacer] = computePanels( const [beforeSpacer, afterSpacer] = computePanels(hass);
hass.panels,
hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
);
let notificationCount = this._notifications let notificationCount = this._notifications
? this._notifications.length ? this._notifications.length
@@ -226,22 +152,10 @@ class HaSidebar extends LitElement {
} }
} }
const defaultPanel = getDefaultPanel(hass);
return html` return html`
${this._editMode <div class="menu">
? html`
<style>
${sortStyles?.cssText}
</style>
`
: ""}
<div
class="menu"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: !this._editMode,
disabled: this._editMode,
})}
>
${!this.narrow ${!this.narrow
? html` ? html`
<mwc-icon-button <mwc-icon-button
@@ -256,66 +170,40 @@ class HaSidebar extends LitElement {
</mwc-icon-button> </mwc-icon-button>
` `
: ""} : ""}
<div class="title"> <span class="title">Home Assistant</span>
${this._editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${hass.localize("ui.sidebar.done")}
</mwc-button>`
: "Home Assistant"}
</div>
</div> </div>
<paper-listbox <paper-listbox
attr-for-selected="data-panel" attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${hass.panelUrl} .selected=${hass.panelUrl}
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut} @focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll} @scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown} @keydown=${this._listboxKeydown}
> >
${this._editMode ${this._renderPanel(
? html`<div id="sortable"> defaultPanel.url_path,
${guard([this._hiddenPanels, this._renderEmptySortable], () => defaultPanel.title || hass.localize("panel.states"),
this._renderEmptySortable defaultPanel.icon,
? "" !defaultPanel.icon ? mdiViewDashboard : undefined
: this._renderPanels(beforeSpacer) )}
${beforeSpacer.map((panel) =>
this._renderPanel(
panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
undefined
)
)} )}
</div>`
: this._renderPanels(beforeSpacer)}
<div class="spacer" disabled></div> <div class="spacer" disabled></div>
${this._editMode && this._hiddenPanels.length
? html` ${afterSpacer.map((panel) =>
${this._hiddenPanels.map((url) => { this._renderPanel(
const panel = this.hass.panels[url]; panel.url_path,
if (!panel) { hass.localize(`panel.${panel.title}`) || panel.title,
return ""; panel.icon,
} undefined
return html`<paper-icon-item )
@click=${this._unhidePanel} )}
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<mwc-icon-button class="hide-panel">
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</mwc-icon-button>
</paper-icon-item>`;
})}
<div class="spacer" disabled></div>
`
: ""}
${this._renderPanels(afterSpacer)}
${this._externalConfig && this._externalConfig.hasSettingsScreen ${this._externalConfig && this._externalConfig.hasSettingsScreen
? html` ? html`
<a <a
@@ -389,11 +277,7 @@ class HaSidebar extends LitElement {
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
> >
<paper-icon-item> <paper-icon-item>
<ha-user-badge <ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge>
slot="item-icon"
.user=${hass.user}
.hass=${hass}
></ha-user-badge>
<span class="item-text"> <span class="item-text">
${hass.user ? hass.user.name : ""} ${hass.user ? hass.user.name : ""}
@@ -411,11 +295,7 @@ class HaSidebar extends LitElement {
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") || 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; return true;
} }
@@ -432,7 +312,6 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl || hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user || hass.user !== oldHass.user ||
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.language !== oldHass.language ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel hass.defaultPanel !== oldHass.defaultPanel
); );
@@ -449,9 +328,6 @@ class HaSidebar extends LitElement {
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
window.addEventListener("hass-edit-sidebar", () =>
this._activateEditMode()
);
} }
protected updated(changedProps) { protected updated(changedProps) {
@@ -463,14 +339,12 @@ class HaSidebar extends LitElement {
return; return;
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; this._rtl = computeRTL(this.hass);
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
if (!SUPPORT_SCROLL_IF_NEEDED) { if (!SUPPORT_SCROLL_IF_NEEDED) {
return; return;
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) { if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected"); const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) { if (selectedEl) {
@@ -484,80 +358,6 @@ class HaSidebar extends LitElement {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
if (ev.detail.action !== "hold") {
return;
}
this._activateEditMode();
}
private async _activateEditMode() {
if (!Sortable) {
const [sortableImport, sortStylesImport] = await Promise.all([
import("sortablejs/modular/sortable.core.esm"),
import("../resources/ha-sortable-style"),
]);
sortStyles = sortStylesImport.sortableStyles;
Sortable = sortableImport.Sortable;
Sortable.mount(sortableImport.OnSpill);
Sortable.mount(sortableImport.AutoScroll());
}
this._editMode = true;
fireEvent(this, "hass-open-menu");
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.currentTarget 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.currentTarget 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) { private _itemMouseEnter(ev: MouseEvent) {
// On keypresses on the listbox, we're going to ignore mouse enter events // 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 // for 100ms so that we ignore it when pressing down arrow scrolls the
@@ -654,21 +454,6 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-toggle-menu"); fireEvent(this, "hass-toggle-menu");
} }
private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
panel.url_path === this.hass.defaultPanel && !panel.icon
? mdiViewDashboard
: undefined
)
);
}
private _renderPanel( private _renderPanel(
urlPath: string, urlPath: string,
title: string | null, title: string | null,
@@ -678,8 +463,8 @@ class HaSidebar extends LitElement {
return html` return html`
<a <a
aria-role="option" aria-role="option"
href=${`/${urlPath}`} href="${`/${urlPath}`}"
data-panel=${urlPath} data-panel="${urlPath}"
tabindex="-1" tabindex="-1"
@mouseenter=${this._itemMouseEnter} @mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
@@ -692,24 +477,13 @@ class HaSidebar extends LitElement {
></ha-svg-icon>` ></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`} : html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span> <span class="item-text">${title}</span>
${this._editMode
? html`<mwc-icon-button
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>`
: ""}
</paper-icon-item> </paper-icon-item>
</a> </a>
`; `;
} }
static get styles(): CSSResult[] { static get styles(): CSSResult {
return [ return css`
haStyleScrollbar,
css`
:host { :host {
height: 100%; height: 100%;
display: block; display: block;
@@ -722,12 +496,9 @@ class HaSidebar extends LitElement {
width: 64px; width: 64px;
} }
:host([expanded]) { :host([expanded]) {
width: calc(256px + env(safe-area-inset-left)); width: 256px;
}
:host([rtl]) {
border-right: 0;
border-left: 1px solid var(--divider-color);
} }
.menu { .menu {
box-sizing: border-box; box-sizing: border-box;
height: 65px; height: 65px;
@@ -741,41 +512,38 @@ class HaSidebar extends LitElement {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
font-size: 20px; font-size: 20px;
align-items: center; 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 { :host([expanded]) .menu {
width: calc(256px + env(safe-area-inset-left)); width: 256px;
}
:host([rtl][expanded]) .menu {
width: calc(256px + env(safe-area-inset-right));
} }
.menu mwc-icon-button { .menu mwc-icon-button {
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);
} }
:host([expanded]) .menu mwc-icon-button { :host([expanded]) .menu mwc-icon-button {
margin-right: 23px; margin-right: 23px;
} }
:host([expanded][rtl]) .menu mwc-icon-button { :host([expanded][_rtl]) .menu mwc-icon-button {
margin-right: 0px; margin-right: 0px;
margin-left: 23px; margin-left: 23px;
} }
.title { .title {
width: 100%;
display: none; display: none;
} }
:host([narrow]) .title {
padding: 0 16px;
}
:host([expanded]) .title { :host([expanded]) .title {
display: initial; display: initial;
} }
.title mwc-button {
width: 100%; paper-listbox::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
paper-listbox::-webkit-scrollbar-thumb {
-webkit-border-radius: 4px;
border-radius: 4px;
background: var(--scrollbar-thumb-color);
} }
paper-listbox { paper-listbox {
@@ -783,15 +551,12 @@ class HaSidebar extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 196px - env(safe-area-inset-bottom)); height: calc(100% - 196px);
overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
background: none; background: none;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) paper-listbox {
margin-left: initial;
margin-right: env(safe-area-inset-right);
} }
a { a {
@@ -815,7 +580,7 @@ class HaSidebar extends LitElement {
:host([expanded]) paper-icon-item { :host([expanded]) paper-icon-item {
width: 240px; width: 240px;
} }
:host([rtl]) paper-icon-item { :host([_rtl]) paper-icon-item {
padding-left: auto; padding-left: auto;
padding-right: 12px; padding-right: 12px;
} }
@@ -891,11 +656,6 @@ class HaSidebar extends LitElement {
} }
.notifications-container { .notifications-container {
display: flex; display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
} }
.notifications { .notifications {
cursor: pointer; cursor: pointer;
@@ -904,23 +664,18 @@ class HaSidebar extends LitElement {
flex: 1; flex: 1;
} }
.profile { .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 { .profile paper-icon-item {
padding-left: 4px; padding-left: 4px;
} }
:host([rtl]) .profile paper-icon-item { :host([_rtl]) .profile paper-icon-item {
padding-left: auto; padding-left: auto;
padding-right: 4px; padding-right: 4px;
} }
.profile .item-text { .profile .item-text {
margin-left: 8px; margin-left: 8px;
} }
:host([rtl]) .profile .item-text { :host([_rtl]) .profile .item-text {
margin-right: 8px; margin-right: 8px;
} }
@@ -933,7 +688,7 @@ class HaSidebar extends LitElement {
line-height: 20px; line-height: 20px;
text-align: center; text-align: center;
padding: 0px 6px; padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color)); color: var(--text-primary-color);
} }
ha-svg-icon + .notification-badge { ha-svg-icon + .notification-badge {
position: absolute; position: absolute;
@@ -980,12 +735,11 @@ class HaSidebar extends LitElement {
font-weight: 500; font-weight: 500;
} }
:host([rtl]) .menu mwc-icon-button { :host([_rtl]) .menu mwc-icon-button {
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
transform: scaleX(-1); transform: scaleX(-1);
} }
`, `;
];
} }
} }

View File

@@ -50,8 +50,6 @@ export class HaYamlEditor extends LitElement {
try { try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : ""; this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error(err);
alert(`There was an error converting to YAML: ${err}`); alert(`There was an error converting to YAML: ${err}`);
} }
afterNextRender(() => { afterNextRender(() => {

View File

@@ -6,7 +6,6 @@ import {
LeafletMouseEvent, LeafletMouseEvent,
Map, Map,
Marker, Marker,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@@ -21,27 +20,21 @@ import {
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { import {
LeafletModuleType, LeafletModuleType,
replaceTileLayer,
setupLeafletMap, setupLeafletMap,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status"; import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone"; import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor") @customElement("ha-location-editor")
class LocationEditor extends LitElement { class LocationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property() public location?: [number, number];
@property({ type: Array }) public location?: [number, number]; @property() public radius?: number;
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string; @property() public radiusColor?: string;
@property() public icon?: string; @property() public icon?: string;
@property({ type: Boolean }) public darkMode?: boolean;
public fitZoom = 16; public fitZoom = 16;
private _iconEl?: DivIcon; private _iconEl?: DivIcon;
@@ -53,8 +46,6 @@ class LocationEditor extends LitElement {
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle; private _locationMarker?: Marker | Circle;
public fitMap(): void { public fitMap(): void {
@@ -106,22 +97,6 @@ class LocationEditor extends LitElement {
if (changedProps.has("icon")) { if (changedProps.has("icon")) {
this._updateIcon(); 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 { private get _mapEl(): HTMLDivElement {
@@ -129,9 +104,9 @@ class LocationEditor extends LitElement {
} }
private async _initMap(): Promise<void> { private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( [this._leafletMap, this.Leaflet] = await setupLeafletMap(
this._mapEl, this._mapEl,
this.darkMode ?? this.hass.themes?.darkMode, false,
Boolean(this.radius) Boolean(this.radius)
); );
this._leafletMap.addEventListener( this._leafletMap.addEventListener(
@@ -279,7 +254,9 @@ class LocationEditor extends LitElement {
} }
#map { #map {
height: 100%; height: 100%;
background: inherit; }
.light {
color: #000000;
} }
.leaflet-edit-move { .leaflet-edit-move {
border-radius: 50%; border-radius: 50%;

View File

@@ -6,7 +6,6 @@ import {
Map, Map,
Marker, Marker,
MarkerOptions, MarkerOptions,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@@ -22,10 +21,8 @@ import { fireEvent } from "../../common/dom/fire_event";
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone"; import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
declare global { declare global {
// for fire event // for fire event
@@ -50,8 +47,6 @@ export interface MarkerLocation {
@customElement("ha-locations-editor") @customElement("ha-locations-editor")
export class HaLocationsEditor extends LitElement { export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public locations?: MarkerLocation[]; @property() public locations?: MarkerLocation[];
public fitZoom = 16; public fitZoom = 16;
@@ -62,8 +57,6 @@ export class HaLocationsEditor extends LitElement {
// eslint-disable-next-line // eslint-disable-next-line
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarkers?: { [key: string]: Marker | Circle }; private _locationMarkers?: { [key: string]: Marker | Circle };
private _circles: { [key: string]: Circle } = {}; private _circles: { [key: string]: Circle } = {};
@@ -123,22 +116,6 @@ export class HaLocationsEditor extends LitElement {
if (changedProps.has("locations")) { if (changedProps.has("locations")) {
this._updateMarkers(); 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 { private get _mapEl(): HTMLDivElement {
@@ -146,9 +123,9 @@ export class HaLocationsEditor extends LitElement {
} }
private async _initMap(): Promise<void> { private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( [this._leafletMap, this.Leaflet] = await setupLeafletMap(
this._mapEl, this._mapEl,
this.hass.themes.darkMode, false,
true true
); );
this._updateMarkers(); this._updateMarkers();
@@ -313,6 +290,9 @@ export class HaLocationsEditor extends LitElement {
#map { #map {
height: 100%; height: 100%;
} }
.light {
color: #000000;
}
.leaflet-marker-draggable { .leaflet-marker-draggable {
cursor: move !important; cursor: move !important;
} }

View File

@@ -1,5 +1,5 @@
import "../ha-icon-button"; import "../ha-icon-button";
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet"; import { Circle, Layer, Map, Marker } from "leaflet";
import { import {
css, css,
CSSResult, CSSResult,
@@ -13,7 +13,6 @@ import {
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
@@ -23,11 +22,11 @@ import { HomeAssistant } from "../../types";
@customElement("ha-map") @customElement("ha-map")
class HaMap extends LitElement { class HaMap extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public entities?: string[]; @property() public entities?: string[];
@property() public darkMode?: boolean; @property() public darkMode = false;
@property() public zoom?: number; @property() public zoom?: number;
@@ -36,8 +35,6 @@ class HaMap extends LitElement {
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
// @ts-ignore // @ts-ignore
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
@@ -125,20 +122,6 @@ class HaMap extends LitElement {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
this._drawEntities(); this._drawEntities();
this._fitMap(); 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
);
} }
} }
@@ -147,9 +130,9 @@ class HaMap extends LitElement {
} }
private async loadMap(): Promise<void> { private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( [this._leafletMap, this.Leaflet] = await setupLeafletMap(
this._mapEl, this._mapEl,
this.darkMode ?? this.hass.themes.darkMode this.darkMode
); );
this._drawEntities(); this._drawEntities();
this._leafletMap.invalidateSize(); this._leafletMap.invalidateSize();
@@ -246,8 +229,7 @@ class HaMap extends LitElement {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className: className: this.darkMode ? "dark" : "light",
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
}), }),
interactive: false, interactive: false,
title, title,

Some files were not shown because too many files have changed in this diff Show More