Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Lovén
813c9014e5 Avoid error in map-card 2021-05-20 16:18:31 +02:00
404 changed files with 5207 additions and 12629 deletions

View File

@@ -28,7 +28,9 @@
"__BUILD__": false, "__BUILD__": false,
"__VERSION__": false, "__VERSION__": false,
"__STATIC_PATH__": false, "__STATIC_PATH__": false,
"Polymer": true "Polymer": true,
"webkitSpeechRecognition": false,
"ResizeObserver": false
}, },
"env": { "env": {
"browser": true, "browser": true,
@@ -104,6 +106,5 @@
"lit/attribute-value-entities": 0 "lit/attribute-value-entities": 0
}, },
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable", "processor": "disable/disable"
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
} }

View File

@@ -6,6 +6,7 @@ on:
- published - published
env: env:
WHEELS_TAG: 3.8-alpine3.12
PYTHON_VERSION: 3.8 PYTHON_VERSION: 3.8
NODE_VERSION: 12.1 NODE_VERSION: 12.1
@@ -63,9 +64,6 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"] arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
tag:
- "3.8-alpine3.12"
- "3.9-alpine3.13"
steps: steps:
- name: Download requirements.txt - name: Download requirements.txt
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
@@ -75,7 +73,7 @@ jobs:
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@master uses: home-assistant/wheels@master
with: with:
tag: ${{ matrix.tag }} tag: ${{ env.WHEELS_TAG }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }} wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}

21
.gitignore vendored
View File

@@ -1,17 +1,10 @@
.DS_Store
.reify-cache
# build
build build
build-translations/* build-translations/*
hass_frontend/*
dist
# yarn
.yarn
yarn-error.log
node_modules/* node_modules/*
npm-debug.log npm-debug.log
.DS_Store
hass_frontend/*
.reify-cache
# Python stuff # Python stuff
*.py[cod] *.py[cod]
@@ -21,8 +14,11 @@ npm-debug.log
# venv stuff # venv stuff
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json
venv/* venv
.venv .venv
lib
bin
dist
# vscode # vscode
.vscode/* .vscode/*
@@ -35,8 +31,9 @@ src/cast/dev_const.ts
# Secrets # Secrets
.lokalise_token .lokalise_token
yarn-error.log
# asdf #asdf
.tool-versions .tool-versions
# Home Assistant config # Home Assistant config

View File

@@ -1,4 +0,0 @@
module.exports = {
require: "test-mocha/testconf.js",
timeout: 10000,
};

View File

@@ -52,7 +52,6 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({ module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false, babelrc: false,
compact: false,
presets: [ presets: [
!latestBuild && [ !latestBuild && [
"@babel/preset-env", "@babel/preset-env",
@@ -80,6 +79,12 @@ module.exports.babelOptions = ({ latestBuild }) => ({
].filter(Boolean), ].filter(Boolean),
}); });
// Are already ES5, cause warnings when babelified.
module.exports.babelExclude = () => [
require.resolve("@mdi/js/mdi.js"),
require.resolve("hls.js"),
];
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");

View File

@@ -1,5 +1,4 @@
// Tasks to run webpack. // Tasks to run webpack.
const fs = require("fs");
const gulp = require("gulp"); const gulp = require("gulp");
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
@@ -19,11 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }), createConfigFunc({ ...params, latestBuild: false }),
]; ];
const isWsl = fs
.readFileSync("/proc/version", "utf-8")
.toLocaleLowerCase()
.includes("microsoft");
/** /**
* @param {{ * @param {{
* compiler: import("webpack").Compiler, * compiler: import("webpack").Compiler,
@@ -85,7 +79,7 @@ const prodBuild = (conf) =>
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{ ignored: /build-translations/, poll: isWsl }, { ignored: /build-translations/ },
doneHandler() doneHandler()
); );
gulp.watch( gulp.watch(
@@ -143,7 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),

View File

@@ -52,11 +52,16 @@ const createRollupConfig = ({
browser: true, browser: true,
rootDir: paths.polymer_dir, rootDir: paths.polymer_dir,
}), }),
commonjs(), commonjs({
namedExports: {
"js-yaml": ["safeDump", "safeLoad"],
},
}),
json(), json(),
babel({ babel({
...bundle.babelOptions({ latestBuild }), ...bundle.babelOptions({ latestBuild }),
extensions, extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled", babelHelpers: isWDS ? "inline" : "bundled",
}), }),
string({ string({

View File

@@ -47,6 +47,7 @@ const createWebpackConfig = ({
rules: [ rules: [
{ {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(),
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }), options: bundle.babelOptions({ latestBuild }),
@@ -115,9 +116,8 @@ const createWebpackConfig = ({
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one // We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
new RegExp( new RegExp(
path.resolve( require.resolve(
paths.polymer_dir, "@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
) )
), ),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js") path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")

View File

@@ -70,7 +70,7 @@ class HaDemo extends HomeAssistantAppEl {
} }
e.preventDefault(); e.preventDefault();
navigate(href); navigate(this, href);
}, },
{ capture: true } { capture: true }
); );

View File

@@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; 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 { load } from "js-yaml"; import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
class DemoCard extends PolymerElement { class DemoCard extends PolymerElement {
@@ -80,7 +80,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild); card.removeChild(card.lastChild);
} }
const el = this._createCardElement(load(config.config)[0]); const el = this._createCardElement(safeLoad(config.config)[0]);
card.appendChild(el); card.appendChild(el);
this._getSize(el); this._getSize(el);
} }

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml"; import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -56,7 +56,7 @@ export class DemoAutomationDescribeAction extends LitElement {
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, conf as any)}</span> <span>${describeAction(this.hass, conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${safeDump(conf)}</pre>
</div> </div>
` `
)} )}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml"; import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -26,7 +26,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
(conf) => html` (conf) => html`
<div class="condition"> <div class="condition">
<span>${describeCondition(conf as any)}</span> <span>${describeCondition(conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${safeDump(conf)}</pre>
</div> </div>
` `
)} )}

View File

@@ -1,4 +1,4 @@
import { dump } from "js-yaml"; import { safeDump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -29,7 +29,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
(conf) => html` (conf) => html`
<div class="trigger"> <div class="trigger">
<span>${describeTrigger(conf as any)}</span> <span>${describeTrigger(conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${safeDump(conf)}</pre>
</div> </div>
` `
)} )}

View File

@@ -28,11 +28,10 @@ const createConfigEntry = (
title, title,
source: "zeroconf", source: "zeroconf",
state: "loaded", state: "loaded",
connection_class: "local_push",
supports_options: false, supports_options: false,
supports_unload: true, supports_unload: true,
disabled_by: null, disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
reason: null, reason: null,
...override, ...override,
}); });
@@ -65,9 +64,6 @@ const configPanelEntry = createConfigEntry("Config Panel", {
const optionsFlowEntry = createConfigEntry("Options Flow", { const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true, supports_options: true,
}); });
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
pref_disable_polling: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", { const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error", state: "setup_error",
}); });
@@ -140,7 +136,6 @@ const configEntries: Array<{
{ items: [loadedEntry] }, { items: [loadedEntry] },
{ items: [configPanelEntry] }, { items: [configPanelEntry] },
{ items: [optionsFlowEntry] }, { items: [optionsFlowEntry] },
{ items: [disabledPollingEntry] },
{ items: [nameAsDomainEntry] }, { items: [nameAsDomainEntry] },
{ items: [longNameEntry] }, { items: [longNameEntry] },
{ items: [longNonBreakingNameEntry] }, { items: [longNonBreakingNameEntry] },

View File

@@ -120,7 +120,7 @@ class HassioAddonRepositoryEl extends LitElement {
} }
private _addonTapped(ev) { private _addonTapped(ev) {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}`); navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -138,7 +138,7 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url"); const repositoryUrl = extractSearchParam("repository_url");
navigate("/hassio/store", { replace: true }); navigate(this, "/hassio/store", true);
if (repositoryUrl) { if (repositoryUrl) {
this._manageRepositories(repositoryUrl); this._manageRepositories(repositoryUrl);
} }

View File

@@ -3,7 +3,6 @@ import { ActionDetail } from "@material/mwc-list";
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 "@polymer/iron-autogrow-textarea/iron-autogrow-textarea"; import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -12,7 +11,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
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/buttons/ha-progress-button";
@@ -28,7 +27,6 @@ import {
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@@ -40,13 +38,6 @@ import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"]; const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
kind: "scalar",
construct: (data) => `!secret ${data}`,
}),
]);
@customElement("hassio-addon-config") @customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement { class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails; @property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -134,7 +125,6 @@ class HassioAddonConfig extends LitElement {
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.schema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`} ></ha-yaml-editor>`}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this._yamlMode || ${!this._yamlMode ||
@@ -279,14 +269,6 @@ class HassioAddonConfig extends LitElement {
this._error = undefined; this._error = undefined;
try { try {
const validation = await validateHassioAddonOption(
this.hass,
this.addon.slug,
this._editor?.value
);
if (!validation.valid) {
throw Error(validation.message);
}
await setHassioAddonOption(this.hass, this.addon.slug, { await setHassioAddonOption(this.hass, this.addon.slug, {
options: this._yamlMode ? this._editor?.value : this._options, options: this._yamlMode ? this._editor?.value : this._options,
}); });

View File

@@ -175,7 +175,7 @@ class HassioAddonDashboard extends LitElement {
if (!validAddon) { if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found"); this._error = this.supervisor.localize("my.error_addon_not_found");
} else { } else {
navigate(`/hassio/addon/${requestedAddon}`, { replace: true }); navigate(this, `/hassio/addon/${requestedAddon}`, true);
} }
} }
} }

View File

@@ -761,7 +761,7 @@ class HassioAddonInfo extends LitElement {
} }
private _openIngress(): void { private _openIngress(): void {
navigate(`/hassio/ingress/${this.addon.slug}`); navigate(this, `/hassio/ingress/${this.addon.slug}`);
} }
private get _computeShowIngressUI(): boolean { private get _computeShowIngressUI(): boolean {
@@ -977,7 +977,6 @@ class HassioAddonInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: this.addon.name, name: this.addon.name,
slug: this.addon.slug,
version: this.addon.version_latest, version: this.addon.version_latest,
snapshotParams: { snapshotParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`, name: `addon_${this.addon.slug}_${this.addon.version}`,
@@ -1052,7 +1051,7 @@ class HassioAddonInfo extends LitElement {
} }
private _openConfiguration(): void { private _openConfiguration(): void {
navigate(`/hassio/addon/${this.addon.slug}/config`); navigate(this, `/hassio/addon/${this.addon.slug}/config`);
} }
private async _uninstallClicked(ev: CustomEvent): Promise<void> { private async _uninstallClicked(ev: CustomEvent): Promise<void> {

View File

@@ -1,54 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-svg-icon";
@customElement("supervisor-formfield-label")
class SupervisorFormfieldLabel extends LitElement {
@property({ type: String }) public label!: string;
@property({ type: String }) public imageUrl?: string;
@property({ type: String }) public iconPath?: string;
@property({ type: String }) public version?: string;
protected render(): TemplateResult {
return html`
${this.imageUrl
? html`<img loading="lazy" .src=${this.imageUrl} class="icon" />`
: this.iconPath
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
: ""}
<span class="label">${this.label}</span>
${this.version
? html`<span class="version">(${this.version})</span>`
: ""}
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
align-items: center;
}
.label {
margin-right: 4px;
}
.version {
color: var(--secondary-text-color);
}
.icon {
max-height: 22px;
max-width: 22px;
margin-right: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-formfield-label": SupervisorFormfieldLabel;
}
}

View File

@@ -64,7 +64,6 @@ class SupervisorMetric extends LitElement {
.value { .value {
width: 48px; width: 48px;
padding-right: 4px; padding-right: 4px;
flex-shrink: 0;
} }
`; `;
} }

View File

@@ -1,450 +0,0 @@
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
HassioFullSnapshotCreateParams,
HassioPartialSnapshotCreateParams,
HassioSnapshotDetail,
} from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
interface CheckboxItem {
slug: string;
checked: boolean;
name: string;
}
interface AddonCheckboxItem extends CheckboxItem {
version: string;
}
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("homeassistant")) {
list.push({
slug: "homeassistant",
name: "Home Assistant configuration",
checked: false,
});
}
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false });
}
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
}
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
};
const _computeAddons = (addons): AddonCheckboxItem[] =>
addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: false,
}))
.sort((a, b) => (a.name > b.name ? 1 : -1));
@customElement("supervisor-snapshot-content")
export class SupervisorSnapshotContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
@property() public snapshotType: HassioSnapshotDetail["type"] = "full";
@property({ attribute: false }) public folders?: CheckboxItem[];
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
@property({ type: Boolean }) public homeAssistant = false;
@property({ type: Boolean }) public snapshotHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property() public snapshotName = "";
@property() public snapshotPassword = "";
@property() public confirmSnapshotPassword = "";
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this.folders = _computeFolders(
this.snapshot
? this.snapshot.folders
: ["homeassistant", "ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.snapshot
? this.snapshot.addons
: this.supervisor?.supervisor.addons
);
this.snapshotType = this.snapshot?.type || "full";
this.snapshotName = this.snapshot?.name || "";
this.snapshotHasPassword = this.snapshot?.protected || false;
}
}
private _localize = (string: string) =>
this.supervisor?.localize(`snapshot.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
protected render(): TemplateResult {
if (!this.onboarding && !this.supervisor) {
return html``;
}
const foldersSection =
this.snapshotType === "partial" ? this._getSection("folders") : undefined;
const addonsSection =
this.snapshotType === "partial" ? this._getSection("addons") : undefined;
return html`
${this.snapshot
? html`<div class="details">
${this.snapshot.type === "full"
? this._localize("full_snapshot")
: this._localize("partial_snapshot")}
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
: this.snapshot.date}
</div>`
: html`<paper-input
name="snapshotName"
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
.value=${this.snapshotName}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>`}
${!this.snapshot || this.snapshot.type === "full"
? html`<div class="sub-header">
${!this.snapshot
? this._localize("type")
: this._localize("select_type")}
</div>
<div class="snapshot-types">
<ha-formfield .label=${this._localize("full_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
name="snapshotType"
.checked=${this.snapshotType === "full"}
>
</ha-radio>
</ha-formfield>
<ha-formfield .label=${this._localize("partial_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
name="snapshotType"
.checked=${this.snapshotType === "partial"}
>
</ha-radio>
</ha-formfield>
</div>`
: ""}
${this.snapshotType === "partial"
? html`<div class="partial-picker">
${this.snapshot && this.snapshot.homeassistant
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.snapshot.homeassistant}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@click=${() => {
this.homeAssistant = !this.homeAssistant;
}}
>
</ha-checkbox>
</ha-formfield>
`
: ""}
${foldersSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${foldersSection.checked}
.indeterminate=${foldersSection.indeterminate}
.section=${"folders"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${foldersSection.templates}</div>
`
: ""}
${addonsSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${addonsSection.checked}
.indeterminate=${addonsSection.indeterminate}
.section=${"addons"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${addonsSection.templates}</div>
`
: ""}
</div> `
: ""}
${this.snapshotType === "partial" &&
(!this.snapshot || this.snapshotHasPassword)
? html`<hr />`
: ""}
${!this.snapshot
? html`<ha-formfield
class="password"
.label=${this._localize("password_protection")}
>
<ha-checkbox
.checked=${this.snapshotHasPassword}
@change=${this._toggleHasPassword}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${this.snapshotHasPassword
? html`
<paper-input
.label=${this._localize("password")}
type="password"
name="snapshotPassword"
.value=${this.snapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>
${!this.snapshot
? html` <paper-input
.label=${this.supervisor?.localize("confirm_password")}
type="password"
name="confirmSnapshotPassword"
.value=${this.confirmSnapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>`
: ""}
`
: ""}
`;
}
static get styles(): CSSResultGroup {
return css`
.partial-picker ha-formfield {
display: block;
}
.partial-picker ha-checkbox {
--mdc-checkbox-touch-target-size: 32px;
}
.partial-picker {
display: block;
margin: 0px -6px;
}
supervisor-formfield-label {
display: inline-flex;
align-items: center;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
.details {
color: var(--secondary-text-color);
}
.section-content {
display: flex;
flex-direction: column;
margin-left: 30px;
}
ha-formfield.password {
display: block;
margin: 0 -14px -16px;
}
.snapshot-types {
display: flex;
margin-left: -13px;
}
.sub-header {
margin-top: 8px;
}
`;
}
public snapshotDetails():
| HassioPartialSnapshotCreateParams
| HassioFullSnapshotCreateParams {
const data: any = {};
if (!this.snapshot) {
data.name = this.snapshotName || formatDate(new Date(), this.hass.locale);
}
if (this.snapshotHasPassword) {
data.password = this.snapshotPassword;
if (!this.snapshot) {
data.confirm_password = this.confirmSnapshotPassword;
}
}
if (this.snapshotType === "full") {
return data;
}
const addons = this.addons
?.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this.folders
?.filter((folder) => folder.checked)
.map((folder) => folder.slug);
if (addons?.length) {
data.addons = addons;
}
if (folders?.length) {
data.folders = folders;
}
if (this.homeAssistant) {
data.homeassistant = this.homeAssistant;
}
return data;
}
private _getSection(section: string) {
const templates: TemplateResult[] = [];
const addons =
section === "addons"
? new Map(
this.supervisor?.addon.addons.map((item) => [item.slug, item])
)
: undefined;
let checkedItems = 0;
this[section].forEach((item) => {
templates.push(html`<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" &&
!this.onboarding &&
atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`
: undefined}
.version=${item.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.item=${item}
.checked=${item.checked}
.section=${section}
@change=${this._updateSectionEntry}
>
</ha-checkbox>
</ha-formfield>`);
if (item.checked) {
checkedItems++;
}
});
const checked = checkedItems === this[section].length;
return {
templates,
checked,
indeterminate: !checked && checkedItems !== 0,
};
}
private _handleRadioValueChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
this[input.name] = input.value;
}
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
const input = ev.currentTarget as PaperInputElement;
this[input.name!] = ev.detail.value;
}
private _toggleHasPassword(): void {
this.snapshotHasPassword = !this.snapshotHasPassword;
}
private _toggleSection(ev): void {
const section = ev.currentTarget.section;
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
(item) => ({
...item,
checked: ev.currentTarget.checked,
})
);
}
private _updateSectionEntry(ev): void {
const item = ev.currentTarget.item;
const section = ev.currentTarget.section;
this[section] = this[section].map((entry) =>
entry.slug === item.slug
? {
...entry,
checked: ev.currentTarget.checked,
}
: entry
);
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-snapshot-content": SupervisorSnapshotContent;
}
}

View File

@@ -96,11 +96,11 @@ class HassioAddons extends LitElement {
} }
private _addonTapped(ev: any): void { private _addonTapped(ev: any): void {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`); navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}/info`);
} }
private _openStore(): void { private _openStore(): void {
navigate("/hassio/store"); navigate(this, "/hassio/store");
} }
} }

View File

@@ -161,7 +161,6 @@ export class HassioUpdate extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -1,194 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/common/search/search-input";
import { compare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => compare(a.name, b.name))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = params;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<mwc-icon-button dialogAction="close">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<search-input
autofocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
mwc-icon-button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--code-font-family, monospace);
}
code {
font-size: 85%;
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};

View File

@@ -45,7 +45,7 @@ class HassioRegistriesDialog extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
.open=${this._opened} .open=${this._opened}
@closed=${this.closeDialog} @closing=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
@@ -244,6 +244,9 @@ class HassioRegistriesDialog extends LitElement {
mwc-list-item span[slot="secondary"] { mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-paper-dropdown-menu {
display: block;
}
`, `,
]; ];
} }

View File

@@ -67,7 +67,7 @@ class HassioRepositoriesDialog extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
.open=${this._opened} .open=${this._opened}
@closed=${this.closeDialog} @closing=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
@@ -150,6 +150,9 @@ class HassioRepositoriesDialog extends LitElement {
mwc-button { mwc-button {
margin-left: 8px; margin-left: 8px;
} }
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
margin: 32px; margin: 32px;

View File

@@ -1,43 +1,91 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDate } from "../../../../src/common/datetime/format_date";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { compare } from "../../../../src/common/string/compare";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-checkbox";
import type { HaCheckbox } from "../../../../src/components/ha-checkbox";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
createHassioFullSnapshot, createHassioFullSnapshot,
createHassioPartialSnapshot, createHassioPartialSnapshot,
HassioFullSnapshotCreateParams,
HassioPartialSnapshotCreateParams,
HassioSnapshot,
} from "../../../../src/data/hassio/snapshot"; } from "../../../../src/data/hassio/snapshot";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/supervisor-snapshot-content";
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot"; import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot";
interface CheckboxItem {
slug: string;
checked: boolean;
name?: string;
version?: string;
}
const folderList = () => [
{
slug: "homeassistant",
checked: true,
},
{ slug: "ssl", checked: true },
{ slug: "share", checked: true },
{ slug: "media", checked: true },
{ slug: "addons/local", checked: true },
];
@customElement("dialog-hassio-create-snapshot") @customElement("dialog-hassio-create-snapshot")
class HassioCreateSnapshotDialog extends LitElement { class HassioCreateSnapshotDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _snapshotName = "";
@state() private _snapshotPassword = "";
@state() private _snapshotHasPassword = false;
@state() private _snapshotType: HassioSnapshot["type"] = "full";
@state() private _dialogParams?: HassioCreateSnapshotDialogParams; @state() private _dialogParams?: HassioCreateSnapshotDialogParams;
@state() private _error?: string; @state() private _addonList: CheckboxItem[] = [];
@state() private _creatingSnapshot = false; @state() private _folderList: CheckboxItem[] = folderList();
@query("supervisor-snapshot-content") @state() private _error = "";
private _snapshotContent!: SupervisorSnapshotContent;
public showDialog(params: HassioCreateSnapshotDialogParams) { public showDialog(params: HassioCreateSnapshotDialogParams) {
this._dialogParams = params; this._dialogParams = params;
this._creatingSnapshot = false; this._addonList = this._dialogParams.supervisor.supervisor.addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: true,
}))
.sort((a, b) => compare(a.name, b.name));
this._snapshotType = "full";
this._error = "";
this._folderList = folderList();
this._snapshotHasPassword = false;
this._snapshotPassword = "";
this._snapshotName = "";
} }
public closeDialog() { public closeDialog() {
this._dialogParams = undefined; this._dialogParams = undefined;
this._creatingSnapshot = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -48,36 +96,179 @@ class HassioCreateSnapshotDialog extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
open open
scrimClickAction @closing=${this.closeDialog}
@closed=${this.closeDialog}
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this._dialogParams.supervisor.localize("snapshot.create_snapshot") this._dialogParams.supervisor.localize("snapshot.create_snapshot")
)} )}
> >
${this._creatingSnapshot <paper-input
? html` <ha-circular-progress active></ha-circular-progress>` name="snapshotName"
: html`<supervisor-snapshot-content .label=${this._dialogParams.supervisor.localize("snapshot.name")}
.hass=${this.hass} .value=${this._snapshotName}
.supervisor=${this._dialogParams.supervisor} @value-changed=${this._handleTextValueChanged}
> >
</supervisor-snapshot-content>`} </paper-input>
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""} <div class="snapshot-types">
<div>
${this._dialogParams.supervisor.localize("snapshot.type")}:
</div>
<ha-formfield
.label=${this._dialogParams.supervisor.localize(
"snapshot.full_snapshot"
)}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
name="snapshotType"
.checked=${this._snapshotType === "full"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this._dialogParams.supervisor.localize(
"snapshot.partial_snapshot"
)}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
name="snapshotType"
.checked=${this._snapshotType === "partial"}
>
</ha-radio>
</ha-formfield>
</div>
${
this._snapshotType === "full"
? undefined
: html`
${this._dialogParams.supervisor.localize("snapshot.folders")}:
<div class="checkbox-section">
${this._folderList.map(
(folder, idx) => html`
<div class="checkbox-line">
<ha-checkbox
.idx=${idx}
.checked=${folder.checked}
@change=${this._folderChecked}
slot="prefix"
>
</ha-checkbox>
<span>
${this._dialogParams!.supervisor.localize(
`snapshot.folder.${folder.slug}`
)}
</span>
</div>
`
)}
</div>
${this._dialogParams.supervisor.localize("snapshot.addons")}:
<div class="checkbox-section">
${this._addonList.map(
(addon, idx) => html`
<div class="checkbox-line">
<ha-checkbox
.idx=${idx}
.checked=${addon.checked}
@change=${this._addonChecked}
slot="prefix"
>
</ha-checkbox>
<span>
${addon.name}<span class="version">
(${addon.version})
</span>
</span>
</div>
`
)}
</div>
`
}
${this._dialogParams.supervisor.localize("snapshot.security")}:
<div class="checkbox-section">
<div class="checkbox-line">
<ha-checkbox
.checked=${this._snapshotHasPassword}
@change=${this._handleCheckboxValueChanged}
slot="prefix"
>
</ha-checkbox>
<span>
${this._dialogParams.supervisor.localize(
"snapshot.password_protection"
)}
</span>
</span>
</div>
</div>
${
this._snapshotHasPassword
? html`
<paper-input
.label=${this._dialogParams.supervisor.localize(
"snapshot.password"
)}
type="password"
name="snapshotPassword"
.value=${this._snapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>
`
: undefined
}
${
this._error !== ""
? html` <p class="error">${this._error}</p> `
: undefined
}
<mwc-button slot="secondaryAction" @click=${this.closeDialog}> <mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this._dialogParams.supervisor.localize("common.close")} ${this._dialogParams.supervisor.localize("common.close")}
</mwc-button> </mwc-button>
<mwc-button <ha-progress-button slot="primaryAction" @click=${this._createSnapshot}>
.disabled=${this._creatingSnapshot}
slot="primaryAction"
@click=${this._createSnapshot}
>
${this._dialogParams.supervisor.localize("snapshot.create")} ${this._dialogParams.supervisor.localize("snapshot.create")}
</mwc-button> </ha-progress-button>
</ha-dialog> </ha-dialog>
`; `;
} }
private async _createSnapshot(): Promise<void> { private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
const input = ev.currentTarget as PaperInputElement;
this[`_${input.name}`] = ev.detail.value;
}
private _handleCheckboxValueChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaCheckbox;
this._snapshotHasPassword = input.checked;
}
private _handleRadioValueChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
this[`_${input.name}`] = input.value;
}
private _folderChecked(ev) {
const { idx, checked } = ev.currentTarget!;
this._folderList = this._folderList.map((folder, curIdx) =>
curIdx === idx ? { ...folder, checked } : folder
);
}
private _addonChecked(ev) {
const { idx, checked } = ev.currentTarget!;
this._addonList = this._addonList.map((addon, curIdx) =>
curIdx === idx ? { ...addon, checked } : addon
);
}
private async _createSnapshot(ev: CustomEvent): Promise<void> {
if (this._dialogParams!.supervisor.info.state !== "running") { if (this._dialogParams!.supervisor.info.state !== "running") {
showAlertDialog(this, { showAlertDialog(this, {
title: this._dialogParams!.supervisor.localize( title: this._dialogParams!.supervisor.localize(
@@ -91,35 +282,40 @@ class HassioCreateSnapshotDialog extends LitElement {
}); });
return; return;
} }
const snapshotDetails = this._snapshotContent.snapshotDetails(); const button = ev.currentTarget as any;
this._creatingSnapshot = true; button.progress = true;
this._error = ""; this._error = "";
if (snapshotDetails.password && !snapshotDetails.password.length) { if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = this._dialogParams!.supervisor.localize( this._error = this._dialogParams!.supervisor.localize(
"snapshot.enter_password" "snapshot.enter_password"
); );
this._creatingSnapshot = false; button.progress = false;
return; return;
} }
if ( const name = this._snapshotName || formatDate(new Date(), this.hass.locale);
snapshotDetails.password &&
snapshotDetails.password !== snapshotDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"snapshot.passwords_not_matching"
);
this._creatingSnapshot = false;
return;
}
delete snapshotDetails.confirm_password;
try { try {
if (this._snapshotContent.snapshotType === "full") { if (this._snapshotType === "full") {
await createHassioFullSnapshot(this.hass, snapshotDetails); const data: HassioFullSnapshotCreateParams = { name };
if (this._snapshotHasPassword) {
data.password = this._snapshotPassword;
}
await createHassioFullSnapshot(this.hass, data);
} else { } else {
await createHassioPartialSnapshot(this.hass, snapshotDetails); const data: HassioPartialSnapshotCreateParams = {
name,
folders: this._folderList
.filter((folder) => folder.checked)
.map((folder) => folder.slug),
addons: this._addonList
.filter((addon) => addon.checked)
.map((addon) => addon.slug),
};
if (this._snapshotHasPassword) {
data.password = this._snapshotPassword;
}
await createHassioPartialSnapshot(this.hass, data);
} }
this._dialogParams!.onCreate(); this._dialogParams!.onCreate();
@@ -127,7 +323,7 @@ class HassioCreateSnapshotDialog extends LitElement {
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
} }
this._creatingSnapshot = false; button.progress = false;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -135,9 +331,22 @@ class HassioCreateSnapshotDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-circular-progress { .error {
color: var(--error-color);
}
paper-input[type="password"] {
display: block; display: block;
text-align: center; margin: 4px 0 4px 16px;
}
span.version {
color: var(--secondary-text-color);
}
.checkbox-section {
display: grid;
}
.checkbox-line {
display: inline-flex;
align-items: center;
} }
`, `,
]; ];

View File

@@ -1,12 +1,12 @@
import { ActionDetail } from "@material/mwc-list"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
import { mdiClose, mdiDotsVertical } from "@mdi/js"; import "@polymer/paper-checkbox/paper-checkbox";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
@@ -15,47 +15,95 @@ import {
fetchHassioSnapshotInfo, fetchHassioSnapshotInfo,
HassioSnapshotDetail, HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot"; } from "../../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box"; } from "../../../../src/dialogs/generic/show-dialog-box";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-snapshot-content";
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot"; import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
const _computeFolders = (folders) => {
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
if (folders.includes("homeassistant")) {
list.push({
slug: "homeassistant",
name: "Home Assistant configuration",
checked: true,
});
}
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: true });
}
if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: true });
}
if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: true });
}
return list;
};
const _computeAddons = (addons) =>
addons.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: true,
}));
interface AddonItem {
slug: string;
name: string;
version: string;
checked: boolean | null | undefined;
}
interface FolderItem {
slug: string;
name: string;
checked: boolean | null | undefined;
}
@customElement("dialog-hassio-snapshot") @customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog class HassioSnapshotDialog extends LitElement {
extends LitElement
implements HassDialog<HassioSnapshotDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor?: Supervisor;
@state() private _error?: string; @state() private _error?: string;
@state() private _onboarding = false;
@state() private _snapshot?: HassioSnapshotDetail; @state() private _snapshot?: HassioSnapshotDetail;
@state() private _folders!: FolderItem[];
@state() private _addons!: AddonItem[];
@state() private _dialogParams?: HassioSnapshotDialogParams; @state() private _dialogParams?: HassioSnapshotDialogParams;
@state() private _restoringSnapshot = false; @state() private _snapshotPassword!: string;
@query("supervisor-snapshot-content") @state() private _restoreHass = true;
private _snapshotContent!: SupervisorSnapshotContent;
public async showDialog(params: HassioSnapshotDialogParams) { public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug); this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this._dialogParams = params; this._folders = _computeFolders(
this._restoringSnapshot = false; this._snapshot?.folders
} ).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
this._addons = _computeAddons(
this._snapshot?.addons
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
public closeDialog() { this._dialogParams = params;
this._snapshot = undefined; this._onboarding = params.onboarding ?? false;
this._dialogParams = undefined; this.supervisor = params.supervisor;
this._restoringSnapshot = false; if (!this._snapshot.homeassistant) {
this._error = undefined; this._restoreHass = false;
fireEvent(this, "dialog-closed", { dialog: this.localName }); }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -63,54 +111,121 @@ class HassioSnapshotDialog
return html``; return html``;
} }
return html` return html`
<ha-dialog <ha-dialog open @closing=${this._closeDialog} .heading=${true}>
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${true}
>
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title">${this._snapshot.name}</span> <span slot="title"> ${this._computeName} </span>
<mwc-icon-button slot="actionItems" dialogAction="cancel"> <mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
${this._restoringSnapshot <div class="details">
? html` <ha-circular-progress active></ha-circular-progress>` ${this._snapshot.type === "full"
: html`<supervisor-snapshot-content ? "Full snapshot"
.hass=${this.hass} : "Partial snapshot"}
.supervisor=${this._dialogParams.supervisor} (${this._computeSize})<br />
.snapshot=${this._snapshot} ${formatDateTime(new Date(this._snapshot.date), this.hass.locale)}
.onboarding=${this._dialogParams.onboarding || false} </div>
.localize=${this._dialogParams.localize} ${this._snapshot.homeassistant
? html`<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
}}"
> >
</supervisor-snapshot-content>`} Home Assistant
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""} <span class="version">(${this._snapshot.homeassistant})</span>
</paper-checkbox>`
<mwc-button
.disabled=${this._restoringSnapshot}
slot="secondaryAction"
@click=${this._restoreClicked}
>
Restore
</mwc-button>
${!this._dialogParams.onboarding
? html`<ha-button-menu
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closed=${(ev: Event) => ev.stopPropagation()}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>Download Snapshot</mwc-list-item>
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
</ha-button-menu>`
: ""} : ""}
${this._folders.length
? html`
<div>Folders:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._folders.map(
(item) => html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateFolders(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
</paper-checkbox>
`
)}
</paper-dialog-scrollable>
`
: ""}
${this._addons.length
? html`
<div>Add-on:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._addons.map(
(item) => html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateAddons(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
<span class="version">(${item.version})</span>
</paper-checkbox>
`
)}
</paper-dialog-scrollable>
`
: ""}
${this._snapshot.protected
? html`
<paper-input
autofocus=""
label="Password"
type="password"
@value-changed=${this._passwordInput}
.value=${this._snapshotPassword}
></paper-input>
`
: ""}
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
<div class="button-row" slot="primaryAction">
<mwc-button @click=${this._partialRestoreClicked}>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Selected
</mwc-button>
${!this._onboarding
? html`
<mwc-button @click=${this._deleteClicked}>
<ha-svg-icon .path=${mdiDelete} class="icon warning">
</ha-svg-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
`
: ""}
</div>
<div class="button-row" slot="secondaryAction">
${this._snapshot.type === "full"
? html`
<mwc-button @click=${this._fullRestoreClicked}>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Everything
</mwc-button>
`
: ""}
${!this._onboarding
? html`<mwc-button @click=${this._downloadClicked}>
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
Download Snapshot
</mwc-button>`
: ""}
</div>
</ha-dialog> </ha-dialog>
`; `;
} }
@@ -120,53 +235,83 @@ class HassioSnapshotDialog
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-svg-icon { paper-checkbox {
color: var(--primary-text-color);
}
ha-circular-progress {
display: block; display: block;
text-align: center; margin: 4px;
}
mwc-button ha-svg-icon {
margin-right: 4px;
}
.button-row {
display: grid;
gap: 8px;
margin-right: 8px;
}
.details {
color: var(--secondary-text-color);
}
.warning,
.error {
color: var(--error-color);
}
.buttons li {
list-style-type: none;
}
.buttons .icon {
margin-right: 16px;
}
.no-margin-top {
margin-top: 0;
}
span.version {
color: var(--secondary-text-color);
} }
ha-header-bar { ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface); --mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
display: block; }
/* 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);
}
} }
`, `,
]; ];
} }
private _handleMenuAction(ev: CustomEvent<ActionDetail>) { private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
switch (ev.detail.index) { this._folders = this._folders.map((folder) => {
case 0: if (folder.slug === item.slug) {
this._downloadClicked(); folder.checked = value;
break;
case 1:
this._deleteClicked();
break;
} }
return folder;
});
} }
private async _restoreClicked() { private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
const snapshotDetails = this._snapshotContent.snapshotDetails(); this._addons = this._addons.map((addon) => {
this._restoringSnapshot = true; if (addon.slug === item.slug) {
if (this._snapshotContent.snapshotType === "full") { addon.checked = value;
await this._fullRestoreClicked(snapshotDetails);
} else {
await this._partialRestoreClicked(snapshotDetails);
} }
this._restoringSnapshot = false; return addon;
});
} }
private async _partialRestoreClicked(snapshotDetails) { private _passwordInput(ev: PolymerChangedEvent<string>) {
this._snapshotPassword = ev.detail.value;
}
private async _partialRestoreClicked() {
if ( if (
this._dialogParams?.supervisor !== undefined && this.supervisor !== undefined &&
this._dialogParams?.supervisor.info.state !== "running" this.supervisor.info.state !== "running"
) { ) {
await showAlertDialog(this, { await showAlertDialog(this, {
title: "Could not restore snapshot", title: "Could not restore snapshot",
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`, text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
}); });
return; return;
} }
@@ -180,17 +325,41 @@ class HassioSnapshotDialog
return; return;
} }
if (!this._dialogParams?.onboarding) { const addons = this._addons
.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this._folders
.filter((folder) => folder.checked)
.map((folder) => folder.slug);
const data: {
homeassistant: boolean;
addons: any;
folders: any;
password?: string;
} = {
homeassistant: this._restoreHass,
addons,
folders,
};
if (this._snapshot!.protected) {
data.password = this._snapshotPassword;
}
if (!this._onboarding) {
this.hass this.hass
.callApi( .callApi(
"POST", "POST",
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`, `hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
snapshotDetails data
) )
.then( .then(
() => { () => {
this.closeDialog(); alert("Snapshot restored!");
this._closeDialog();
}, },
(error) => { (error) => {
this._error = error.body.message; this._error = error.body.message;
@@ -200,20 +369,20 @@ class HassioSnapshotDialog
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, { fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
method: "POST", method: "POST",
body: JSON.stringify(snapshotDetails), body: JSON.stringify(data),
}); });
this.closeDialog(); this._closeDialog();
} }
} }
private async _fullRestoreClicked(snapshotDetails) { private async _fullRestoreClicked() {
if ( if (
this._dialogParams?.supervisor !== undefined && this.supervisor !== undefined &&
this._dialogParams?.supervisor.info.state !== "running" this.supervisor.info.state !== "running"
) { ) {
await showAlertDialog(this, { await showAlertDialog(this, {
title: "Could not restore snapshot", title: "Could not restore snapshot",
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`, text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
}); });
return; return;
} }
@@ -228,16 +397,20 @@ class HassioSnapshotDialog
return; return;
} }
if (!this._dialogParams?.onboarding) { const data = this._snapshot!.protected
? { password: this._snapshotPassword }
: undefined;
if (!this._onboarding) {
this.hass this.hass
.callApi( .callApi(
"POST", "POST",
`hassio/snapshots/${this._snapshot!.slug}/restore/full`, `hassio/snapshots/${this._snapshot!.slug}/restore/full`,
snapshotDetails data
) )
.then( .then(
() => { () => {
this.closeDialog(); alert("Snapshot restored!");
this._closeDialog();
}, },
(error) => { (error) => {
this._error = error.body.message; this._error = error.body.message;
@@ -247,9 +420,9 @@ class HassioSnapshotDialog
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, { fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
method: "POST", method: "POST",
body: JSON.stringify(snapshotDetails), body: JSON.stringify(data),
}); });
this.closeDialog(); this._closeDialog();
} }
} }
@@ -272,7 +445,7 @@ class HassioSnapshotDialog
if (this._dialogParams!.onDelete) { if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete(); this._dialogParams!.onDelete();
} }
this.closeDialog(); this._closeDialog();
}, },
(error) => { (error) => {
this._error = error.body.message; this._error = error.body.message;
@@ -288,9 +461,7 @@ class HassioSnapshotDialog
`/api/hassio/snapshots/${this._snapshot!.slug}/download` `/api/hassio/snapshots/${this._snapshot!.slug}/download`
); );
} catch (err) { } catch (err) {
await showAlertDialog(this, { alert(`Error: ${extractApiErrorMessage(err)}`);
text: extractApiErrorMessage(err),
});
return; return;
} }
@@ -307,11 +478,13 @@ class HassioSnapshotDialog
} }
} }
fileDownload( const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
this, const a = document.createElement("a");
signedPath.path, a.href = signedPath.path;
`home_assistant_snapshot_${slugify(this._computeName)}.tar` a.download = `Hass_io_${name}.tar`;
); this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);
} }
private get _computeName() { private get _computeName() {
@@ -319,6 +492,18 @@ class HassioSnapshotDialog
? this._snapshot.name || this._snapshot.slug ? this._snapshot.name || this._snapshot.slug
: "Unnamed snapshot"; : "Unnamed snapshot";
} }
private get _computeSize() {
return Math.ceil(this._snapshot!.size * 10) / 10 + " MB";
}
private _closeDialog() {
this._dialogParams = undefined;
this._snapshot = undefined;
this._snapshotPassword = "";
this._folders = [];
this._addons = [];
}
} }
declare global { declare global {

View File

@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LocalizeFunc } from "../../../../src/common/translations/localize";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioSnapshotDialogParams { export interface HassioSnapshotDialogParams {
@@ -7,7 +6,6 @@ export interface HassioSnapshotDialogParams {
onDelete?: () => void; onDelete?: () => void;
onboarding?: boolean; onboarding?: boolean;
supervisor?: Supervisor; supervisor?: Supervisor;
localize?: LocalizeFunc;
} }
export const showHassioSnapshotDialog = ( export const showHassioSnapshotDialog = (

View File

@@ -2,32 +2,19 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-checkbox";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
} from "../../../../src/data/hassio/common"; } from "../../../../src/data/hassio/common";
import {
SupervisorFrontendPrefrences,
fetchSupervisorFrontendPreferences,
saveSupervisorFrontendPreferences,
} from "../../../../src/data/supervisor/supervisor";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot"; import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
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";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update"; import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
import memoizeOne from "memoize-one";
const snapshot_before_update = memoizeOne(
(slug: string, frontendPrefrences: SupervisorFrontendPrefrences) =>
slug in frontendPrefrences.snapshot_before_update
? frontendPrefrences.snapshot_before_update[slug]
: true
);
@customElement("dialog-supervisor-update") @customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement { class DialogSupervisorUpdate extends LitElement {
@@ -35,12 +22,12 @@ class DialogSupervisorUpdate extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@state() private _createSnapshot = true;
@state() private _action: "snapshot" | "update" | null = null; @state() private _action: "snapshot" | "update" | null = null;
@state() private _error?: string; @state() private _error?: string;
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
@state() @state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams; private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
@@ -49,17 +36,14 @@ class DialogSupervisorUpdate extends LitElement {
): Promise<void> { ): Promise<void> {
this._opened = true; this._opened = true;
this._dialogParams = params; this._dialogParams = params;
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
this.hass
);
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._action = null; this._action = null;
this._createSnapshot = true;
this._error = undefined; this._error = undefined;
this._dialogParams = undefined; this._dialogParams = undefined;
this._frontendPrefrences = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -72,7 +56,7 @@ class DialogSupervisorUpdate extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._dialogParams || !this._frontendPrefrences) { if (!this._dialogParams) {
return html``; return html``;
} }
return html` return html`
@@ -98,16 +82,6 @@ class DialogSupervisorUpdate extends LitElement {
</div> </div>
<ha-settings-row> <ha-settings-row>
<ha-checkbox
.checked=${snapshot_before_update(
this._dialogParams.slug,
this._frontendPrefrences
)}
haptic
@click=${this._toggleSnapshot}
slot="prefix"
>
</ha-checkbox>
<span slot="heading"> <span slot="heading">
${this._dialogParams.supervisor.localize( ${this._dialogParams.supervisor.localize(
"dialog.update.snapshot" "dialog.update.snapshot"
@@ -120,6 +94,12 @@ class DialogSupervisorUpdate extends LitElement {
this._dialogParams.name this._dialogParams.name
)} )}
</span> </span>
<ha-switch
.checked=${this._createSnapshot}
haptic
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row> </ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction"> <mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")} ${this._dialogParams.supervisor.localize("common.cancel")}
@@ -153,27 +133,12 @@ class DialogSupervisorUpdate extends LitElement {
`; `;
} }
private async _toggleSnapshot(): Promise<void> { private _toggleSnapshot() {
this._frontendPrefrences!.snapshot_before_update[ this._createSnapshot = !this._createSnapshot;
this._dialogParams!.slug
] = !snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
);
await saveSupervisorFrontendPreferences(
this.hass,
this._frontendPrefrences!
);
} }
private async _update() { private async _update() {
if ( if (this._createSnapshot) {
snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
)
) {
this._action = "snapshot"; this._action = "snapshot";
try { try {
await createHassioPartialSnapshot( await createHassioPartialSnapshot(

View File

@@ -5,7 +5,6 @@ export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor; supervisor: Supervisor;
name: string; name: string;
version: string; version: string;
slug: string;
snapshotParams: any; snapshotParams: any;
updateHandler: () => Promise<void>; updateHandler: () => Promise<void>;
} }

View File

@@ -15,11 +15,5 @@ body {
padding: 0; padding: 0;
height: 100vh; height: 100vh;
} }
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
}
}
`; `;
document.head.appendChild(styleEl); document.head.appendChild(styleEl);

View File

@@ -4,7 +4,6 @@ import { atLeastVersion } from "../../src/common/config/version";
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 { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { mainWindow } from "../../src/common/dom/get_main_window";
import { navigate } from "../../src/common/navigate"; import { navigate } from "../../src/common/navigate";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor";
@@ -51,7 +50,7 @@ export class HassioMain extends SupervisorBaseElement {
// Joakim - April 26, 2021 // Joakim - April 26, 2021
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element // Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
mainWindow.addEventListener("location-changed", (ev) => top.addEventListener("location-changed", (ev) =>
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail, { fireEvent(this, ev.type, ev.detail, {
bubbles: false, bubbles: false,
@@ -63,7 +62,7 @@ export class HassioMain extends SupervisorBaseElement {
document.body.addEventListener("click", (ev) => { document.body.addEventListener("click", (ev) => {
const href = isNavigationClick(ev); const href = isNavigationClick(ev);
if (href) { if (href) {
navigate(href); navigate(document.body, href);
} }
}); });
@@ -103,7 +102,7 @@ export class HassioMain extends SupervisorBaseElement {
private _applyTheme() { private _applyTheme() {
let themeName: string; let themeName: string;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined; let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) { if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName = themeName =
@@ -112,9 +111,9 @@ export class HassioMain extends SupervisorBaseElement {
? this.hass.themes.default_dark_theme! ? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme; options = this.hass.selectedTheme;
if (themeSettings?.dark === undefined) { if (themeName === "default" && options?.dark === undefined) {
themeSettings = { options = {
...this.hass.selectedTheme, ...this.hass.selectedTheme,
dark: this.hass.themes.darkMode, dark: this.hass.themes.darkMode,
}; };
@@ -129,7 +128,7 @@ export class HassioMain extends SupervisorBaseElement {
this.parentElement, this.parentElement,
this.hass.themes, this.hass.themes,
themeName, themeName,
themeSettings options
); );
} }
} }

View File

@@ -89,7 +89,7 @@ class HassioMyRedirect extends LitElement {
return; return;
} }
navigate(url, { replace: true }); navigate(this, url, true);
} }
protected render(): TemplateResult { protected render(): TemplateResult {

View File

@@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status";
import { import {
fetchHassioAddonInfo, fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
@@ -96,26 +95,17 @@ class HassioIngressView extends LitElement {
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
title: requestedAddon, title: requestedAddon,
}); });
await nextRender(); history.back();
navigate("/hassio/store", { replace: true });
return; return;
} }
if (!addonInfo.version) { if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, { await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"), text: this.supervisor.localize("my.error_addon_no_ingress"),
title: addonInfo.name, title: addonInfo.name,
}); });
await nextRender(); history.back();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else { } else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true }); navigate(this, `/hassio/ingress/${addonInfo.slug}`, true);
} }
} }
} }
@@ -150,7 +140,6 @@ class HassioIngressView extends LitElement {
text: "Unable to fetch add-on info to start Ingress", text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor", title: "Supervisor",
}); });
await nextRender();
history.back(); history.back();
return; return;
} }
@@ -160,7 +149,6 @@ class HassioIngressView extends LitElement {
text: "Add-on does not support Ingress", text: "Add-on does not support Ingress",
title: addon.name, title: addon.name,
}); });
await nextRender();
history.back(); history.back();
return; return;
} }
@@ -170,8 +158,7 @@ class HassioIngressView extends LitElement {
text: "Add-on is not running. Please start it first", text: "Add-on is not running. Please start it first",
title: addon.name, title: addon.name,
}); });
await nextRender(); navigate(this, `/hassio/addon/${addon.slug}/info`, true);
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return; return;
} }
@@ -184,7 +171,6 @@ class HassioIngressView extends LitElement {
text: "Unable to create an Ingress session", text: "Unable to create an Ingress session",
title: addon.name, title: addon.name,
}); });
await nextRender();
history.back(); history.back();
return; return;
} }

View File

@@ -1,17 +1,15 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import relativeTime from "../../../src/common/datetime/relative_time"; import relativeTime from "../../../src/common/datetime/relative_time";
@@ -19,25 +17,18 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
SelectionChangedEvent,
} from "../../../src/components/data-table/ha-data-table"; } from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
fetchHassioSnapshots, fetchHassioSnapshots,
friendlyFolderName, friendlyFolderName,
HassioSnapshot, HassioSnapshot,
reloadHassioSnapshots, reloadHassioSnapshots,
removeSnapshot,
} from "../../../src/data/hassio/snapshot"; } from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-tabs-subpage-data-table"; import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot"; import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
@@ -58,15 +49,10 @@ export class HassioSnapshots extends LitElement {
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@state() private _selectedSnapshots: string[] = []; private _firstUpdatedCalled = false;
@state() private _snapshots?: HassioSnapshot[] = []; @state() private _snapshots?: HassioSnapshot[] = [];
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _firstUpdatedCalled = false;
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) { if (this.hass && this._firstUpdatedCalled) {
@@ -167,9 +153,7 @@ export class HassioSnapshots extends LitElement {
.data=${this._snapshotData(this._snapshots || [])} .data=${this._snapshotData(this._snapshots || [])}
id="slug" id="slug"
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
@selection-changed=${this._handleSelectionChanged}
clickable clickable
selectable
hasFab hasFab
main-page main-page
supervisor supervisor
@@ -192,45 +176,6 @@ export class HassioSnapshots extends LitElement {
: ""} : ""}
</ha-button-menu> </ha-button-menu>
${this._selectedSnapshots.length
? html`<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.supervisor.localize("snapshot.selected", {
number: this._selectedSnapshots.length,
})}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button
@click=${this._deleteSelected}
class="warning"
>
${this.supervisor.localize("snapshot.delete_selected")}
</mwc-button>
`
: html`
<mwc-icon-button
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("snapshot.delete_selected")}
</paper-tooltip>
`}
</div>
</div> `
: ""}
<ha-fab <ha-fab
slot="fab" slot="fab"
@click=${this._createSnapshot} @click=${this._createSnapshot}
@@ -254,12 +199,6 @@ export class HassioSnapshots extends LitElement {
} }
} }
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedSnapshots = ev.detail.value;
}
private _showUploadSnapshotDialog() { private _showUploadSnapshotDialog() {
showSnapshotUploadDialog(this, { showSnapshotUploadDialog(this, {
showSnapshot: (slug: string) => showSnapshot: (slug: string) =>
@@ -277,35 +216,6 @@ export class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass); this._snapshots = await fetchHassioSnapshots(this.hass);
} }
private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, {
title: this.supervisor.localize("snapshot.delete_snapshot_title"),
text: this.supervisor.localize("snapshot.delete_snapshot_text", {
number: this._selectedSnapshots.length,
}),
confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"),
});
if (!confirm) {
return;
}
try {
await Promise.all(
this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug))
);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("snapshot.failed_to_delete"),
text: extractApiErrorMessage(err),
});
return;
}
await reloadHassioSnapshots(this.hass);
this._snapshots = await fetchHassioSnapshots(this.hass);
this._dataTable.clearSelection();
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const slug = ev.detail.id; const slug = ev.detail.id;
showHassioSnapshotDialog(this, { showHassioSnapshotDialog(this, {
@@ -334,45 +244,7 @@ export class HassioSnapshots extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [haStyle, hassioStyle];
haStyle,
hassioStyle,
css`
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 58px;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
color: var(--primary-text-color);
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
}
.header-btns > mwc-button,
.header-btns > mwc-icon-button {
margin: 8px;
}
`,
];
} }
} }

View File

@@ -164,7 +164,6 @@ class HassioCoreInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; 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 { safeDump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -40,8 +41,8 @@ import {
roundWithOneDecimal, roundWithOneDecimal,
} from "../../../src/util/calculate"; } from "../../../src/util/calculate";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info") @customElement("hassio-host-info")
@@ -228,19 +229,20 @@ class HassioHostInfo extends LitElement {
} }
private async _showHardware(): Promise<void> { private async _showHardware(): Promise<void> {
let hardware;
try { try {
hardware = await fetchHassioHardwareInfo(this.hass); const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) { } catch (err) {
await showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list" "system.host.failed_to_get_hardware_list"
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
return;
} }
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
} }
private async _hostReboot(ev: CustomEvent): Promise<void> { private async _hostReboot(ev: CustomEvent): Promise<void> {

View File

@@ -1,4 +1,5 @@
module.exports = { module.exports = {
"*.ts": () => "tsc -p tsconfig.json",
"*.{js,ts}": "eslint --fix", "*.{js,ts}": "eslint --fix",
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write", "!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
}; };

View File

@@ -16,13 +16,13 @@
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md", "lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types", "lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier", "format": "yarn run format:eslint && yarn run format:prettier",
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "yarn run lint && yarn run mocha" "test": "yarn run lint && yarn run mocha"
}, },
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.1", "@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0", "@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0", "@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.0", "@codemirror/highlight": "^0.18.0",
@@ -35,8 +35,8 @@
"@codemirror/text": "^0.18.0", "@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0", "@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.5.10", "@formatjs/intl-getcanonicallocales": "^1.5.10",
"@formatjs/intl-locale": "^2.4.28", "@formatjs/intl-locale": "^2.4.24",
"@formatjs/intl-pluralrules": "^4.0.22", "@formatjs/intl-pluralrules": "^4.0.18",
"@fullcalendar/common": "5.1.0", "@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0", "@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0", "@fullcalendar/daygrid": "5.1.0",
@@ -66,9 +66,12 @@
"@polymer/iron-autogrow-textarea": "^3.0.1", "@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2", "@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1", "@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1",
@@ -98,26 +101,25 @@
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"chartjs-chart-timeline": "^0.4.0", "chartjs-chart-timeline": "^0.4.0",
"comlink": "^4.3.1", "comlink": "^4.3.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"cropperjs": "^1.5.11", "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",
"fecha": "^4.2.0", "fecha": "^4.2.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.0.5", "hls.js": "^1.0.3",
"home-assistant-js-websocket": "^5.10.0", "home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^5.0.5", "idb-keyval": "^3.2.0",
"intl-messageformat": "^9.6.16", "intl-messageformat": "^9.6.13",
"js-yaml": "^4.1.0", "js-yaml": "^3.13.1",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.0.0-rc.2", "lit": "^2.0.0-rc.2",
"lit-vaadin-helpers": "^0.1.3", "marked": "2.0.0",
"marked": "^2.0.5",
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.2.1", "memoize-one": "^5.0.2",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
@@ -127,12 +129,12 @@
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
"superstruct": "^0.15.2", "superstruct": "^0.15.2",
"tinykeys": "^1.1.3", "tinykeys": "^1.1.1",
"tsparticles": "^1.19.2", "tsparticles": "^1.19.2",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vis-data": "^7.1.2", "vis-data": "^7.1.1",
"vis-network": "^8.5.4", "vis-network": "^8.5.4",
"vue": "^2.6.12", "vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2", "web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5", "workbox-cacheable-response": "^6.1.5",
@@ -144,7 +146,7 @@
"xss": "^1.0.9" "xss": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@babel/core": "^7.14.0",
"@babel/plugin-external-helpers": "^7.12.13", "@babel/plugin-external-helpers": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15", "@babel/plugin-proposal-decorators": "^7.13.15",
@@ -153,7 +155,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.14.2", "@babel/preset-env": "^7.14.0",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2", "@open-wc/dev-server-hmr": "^0.0.2",
@@ -162,13 +164,16 @@
"@rollup/plugin-json": "^4.0.3", "@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2", "@rollup/plugin-replace": "^2.3.2",
"@types/chromecast-caf-receiver": "5.0.12", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.3",
"@types/js-yaml": "^4.0.1", "@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.7.0", "@types/leaflet": "^1.7.0",
"@types/leaflet-draw": "^1.0.3", "@types/leaflet-draw": "^1.0.3",
"@types/marked": "^2.0.3", "@types/marked": "^1.2.2",
"@types/mocha": "^8.2.2", "@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6", "@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
@@ -176,7 +181,7 @@
"@web/dev-server": "^0.0.24", "@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"chai": "^4.3.4", "chai": "^4.2.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"del": "^4.0.0", "del": "^4.0.0",
"eslint": "^7.25.0", "eslint": "^7.25.0",
@@ -190,7 +195,7 @@
"eslint-plugin-wc": "^1.3.0", "eslint-plugin-wc": "^1.3.0",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"gulp": "^4.0.2", "gulp": "^4.0.0",
"gulp-foreach": "^0.1.0", "gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6", "gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
@@ -204,7 +209,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": "^8.4.0", "mocha": "^7.2.0",
"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",
@@ -214,13 +219,13 @@
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4", "rollup-plugin-visualizer": "^4.0.4",
"serve": "^11.3.2", "serve": "^11.3.0",
"sinon": "^11.0.0", "sinon": "^7.3.1",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.2", "terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^8.0.0", "ts-mocha": "^7.0.0",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20210603.0", version="20210518.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

@@ -1,32 +1,21 @@
import { format } from "fecha"; import { format } from "fecha";
import memoizeOne from "memoize-one"; import { FrontendTranslationData } from "../../data/translation";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support"; import { toLocaleDateStringSupportsOptions } from "./check_options_support";
const formatDateMem = memoizeOne( export const formatDate = toLocaleDateStringSupportsOptions
(locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
new Intl.DateTimeFormat(locale.language, { dateObj.toLocaleDateString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
}) })
);
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "longDate"); : (dateObj: Date) => format(dateObj, "longDate");
const formatDateWeekdayMem = memoizeOne( export const formatDateWeekday = toLocaleDateStringSupportsOptions
(locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
new Intl.DateTimeFormat(locale.language, { dateObj.toLocaleDateString(locales.language, {
weekday: "long", weekday: "long",
month: "long", month: "short",
day: "numeric", day: "numeric",
}) })
);
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "dddd, MMM D"); : (dateObj: Date) => format(dateObj, "dddd, MMM D");

View File

@@ -1,42 +1,26 @@
import { format } from "fecha"; import { format } from "fecha";
import memoizeOne from "memoize-one"; import { FrontendTranslationData } from "../../data/translation";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support"; import { toLocaleStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
const formatDateTimeMem = memoizeOne( export const formatDateTime = toLocaleStringSupportsOptions
(locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
new Intl.DateTimeFormat(locale.language, { dateObj.toLocaleString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hour12: useAmPm(locale),
}) })
); : (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
export const formatDateTime = toLocaleStringSupportsOptions export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
formatDateTimeMem(locale).format(dateObj) dateObj.toLocaleString(locales.language, {
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hour12: useAmPm(locale),
}) })
); : (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeWithSecondsMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");

View File

@@ -1,52 +1,29 @@
import { format } from "fecha"; import { format } from "fecha";
import memoizeOne from "memoize-one"; import { FrontendTranslationData } from "../../data/translation";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support"; import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
);
export const formatTime = toLocaleTimeStringSupportsOptions export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
formatTimeMem(locale).format(dateObj) dateObj.toLocaleTimeString(locales.language, {
: (dateObj: Date, locale: FrontendLocaleData) => hour: "numeric",
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : ""); minute: "2-digit",
})
: (dateObj: Date) => format(dateObj, "shortTime");
const formatTimeWithSecondsMem = memoizeOne( export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
(locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
new Intl.DateTimeFormat(locale.language, { dateObj.toLocaleTimeString(locales.language, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hour12: useAmPm(locale),
}) })
); : (dateObj: Date) => format(dateObj, "mediumTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) => ? (dateObj: Date, locales: FrontendTranslationData) =>
formatTimeWithSecondsMem(locale).format(dateObj) dateObj.toLocaleTimeString(locales.language, {
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long", weekday: "long",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hour12: useAmPm(locale),
}) })
); : (dateObj: Date) => format(dateObj, "dddd, HH:mm");
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWeekdayMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");

View File

@@ -1,15 +0,0 @@
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
export const useAmPm = (locale: FrontendLocaleData): boolean => {
if (
locale.time_format === TimeFormat.language ||
locale.time_format === TimeFormat.system
) {
const testLanguage =
locale.time_format === TimeFormat.language ? locale.language : undefined;
const test = new Date().toLocaleString(testLanguage);
return test.includes("AM") || test.includes("PM");
}
return locale.time_format === TimeFormat.am_pm;
};

View File

@@ -1,4 +1,4 @@
import { ThemeVars } from "../../data/ws-themes"; import { Theme } from "../../data/ws-themes";
import { darkStyles, derivedStyles } from "../../resources/styles"; import { darkStyles, derivedStyles } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { import {
@@ -23,60 +23,50 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
* Apply a theme to an element by setting the CSS variables on it. * Apply a theme to an element by setting the CSS variables on it.
* *
* element: Element to apply theme on. * element: Element to apply theme on.
* themes: HASS theme information. * themes: HASS Theme information
* selectedTheme: Selected theme. * selectedTheme: selected theme.
* themeSettings: Settings such as selected dark mode and colors.
*/ */
export const applyThemesOnElement = ( export const applyThemesOnElement = (
element, element,
themes: HomeAssistant["themes"], themes: HomeAssistant["themes"],
selectedTheme?: string, selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]> themeOptions?: Partial<HomeAssistant["selectedTheme"]>
) => { ) => {
let cacheKey = selectedTheme; let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {}; let themeRules: Partial<Theme> = {};
if (themeSettings) { if (selectedTheme === "default" && themeOptions) {
if (themeSettings.dark) { if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`; cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles }; themeRules = darkStyles;
} if (themeOptions.primaryColor) {
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend( themeRules["app-header-background-color"] = hexBlend(
primaryColor, themeOptions.primaryColor,
"#121212", "#121212",
8 8
); );
} }
}
if (primaryColor) { if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${primaryColor}`; cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
const rgbPrimaryColor = hex2rgb(primaryColor); const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor); const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = primaryColor; themeRules["primary-color"] = themeOptions.primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor)); const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor); themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor)); themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] = themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121"; rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] = themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6 rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
? "#fff" ? "#fff"
: "#212121"; : "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"]; themeRules["state-icon-color"] = themeRules["dark-primary-color"];
} }
if (accentColor) { if (themeOptions.accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`; cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
themeRules["accent-color"] = accentColor; themeRules["accent-color"] = themeOptions.accentColor;
const rgbAccentColor = hex2rgb(accentColor); const rgbAccentColor = hex2rgb(themeOptions.accentColor);
themeRules["text-accent-color"] = themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121"; rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
} }
@@ -86,27 +76,9 @@ export const applyThemesOnElement = (
return; return;
} }
} }
}
// Custom theme logic (not relevant for default theme, since it would override if (selectedTheme && themes.themes[selectedTheme]) {
// the derived calculations from above) themeRules = themes.themes[selectedTheme];
if (
selectedTheme &&
selectedTheme !== "default" &&
themes.themes[selectedTheme]
) {
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
themeRules = { ...themeRules, ...baseThemeRules };
// Apply theme vars for the specific mode if available
if (modes) {
if (themeSettings?.dark) {
themeRules = { ...themeRules, ...modes.dark };
} else {
themeRules = { ...themeRules, ...modes.light };
}
}
} }
if (!element._themes?.keys && !Object.keys(themeRules).length) { if (!element._themes?.keys && !Object.keys(themeRules).length) {
@@ -134,12 +106,12 @@ export const applyThemesOnElement = (
const processTheme = ( const processTheme = (
cacheKey: string, cacheKey: string,
theme: Partial<ThemeVars> theme: Partial<Theme>
): ProcessedTheme | undefined => { ): ProcessedTheme | undefined => {
if (!theme || !Object.keys(theme).length) { if (!theme || !Object.keys(theme).length) {
return undefined; return undefined;
} }
const combinedTheme: Partial<ThemeVars> = { const combinedTheme: Partial<Theme> = {
...derivedStyles, ...derivedStyles,
...theme, ...theme,
}; };

View File

@@ -1,8 +0,0 @@
import { MAIN_WINDOW_NAME } from "../../data/main_window";
export const mainWindow =
window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top;

View File

@@ -6,7 +6,8 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement,
darkMode?: boolean darkMode?: boolean,
draw = false
): Promise<[Map, LeafletModuleType, TileLayer]> => { ): Promise<[Map, LeafletModuleType, TileLayer]> => {
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");
@@ -16,6 +17,10 @@ export const setupLeafletMap = async (
.default as LeafletModuleType; .default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import("leaflet-draw");
}
const map = Leaflet.map(mapElement); const map = Leaflet.map(mapElement);
const style = document.createElement("link"); const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css"); style.setAttribute("href", "/static/images/leaflet/leaflet.css");

View File

@@ -1,7 +1,14 @@
/* eslint-disable */
// @ts-ignore
export const SpeechRecognition = export const SpeechRecognition =
// @ts-ignore
window.SpeechRecognition || window.webkitSpeechRecognition; window.SpeechRecognition || window.webkitSpeechRecognition;
// @ts-ignore
export const SpeechGrammarList = export const SpeechGrammarList =
// @ts-ignore
window.SpeechGrammarList || window.webkitSpeechGrammarList; window.SpeechGrammarList || window.webkitSpeechGrammarList;
// @ts-ignore
export const SpeechRecognitionEvent = export const SpeechRecognitionEvent =
// @ts-expect-error // @ts-ignore
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent; window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
/* eslint-enable */

View File

@@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendTranslationData } from "../../data/translation";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
@@ -11,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendTranslationData,
state?: string state?: string
): string => { ): string => {
const compareState = state !== undefined ? state : stateObj.state; const compareState = state !== undefined ? state : stateObj.state;

View File

@@ -89,6 +89,8 @@ export const domainIcon = (
} }
// eslint-disable-next-line // eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`); console.warn(
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
);
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -0,0 +1,19 @@
import { HassEntity } from "home-assistant-js-websocket";
import durationToSeconds from "../datetime/duration_to_seconds";
export const timerTimeRemaining = (
stateObj: HassEntity
): undefined | number => {
if (!stateObj.attributes.remaining) {
return undefined;
}
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
if (stateObj.state === "active") {
const now = new Date().getTime();
const madeActive = new Date(stateObj.last_changed).getTime();
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
}
return timeRemaining;
};

View File

@@ -1,40 +1,35 @@
import { fireEvent } from "./dom/fire_event"; import { fireEvent } from "./dom/fire_event";
import { mainWindow } from "./dom/get_main_window";
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"location-changed": NavigateOptions; "location-changed": {
replace: boolean;
};
} }
} }
export interface NavigateOptions { export const navigate = (_node: any, path: string, replace = false) => {
replace?: boolean;
}
export const navigate = (path: string, options?: NavigateOptions) => {
const replace = options?.replace || false;
if (__DEMO__) { if (__DEMO__) {
if (replace) { if (replace) {
mainWindow.history.replaceState( top.history.replaceState(
mainWindow.history.state?.root ? { root: true } : null, top.history.state?.root ? { root: true } : null,
"", "",
`${mainWindow.location.pathname}#${path}` `${top.location.pathname}#${path}`
); );
} else { } else {
mainWindow.location.hash = path; top.location.hash = path;
} }
} else if (replace) { } else if (replace) {
mainWindow.history.replaceState( top.history.replaceState(
mainWindow.history.state?.root ? { root: true } : null, top.history.state?.root ? { root: true } : null,
"", "",
path path
); );
} else { } else {
mainWindow.history.pushState(null, "", path); top.history.pushState(null, "", path);
} }
fireEvent(mainWindow, "location-changed", { fireEvent(top, "location-changed", {
replace, replace,
}); });
}; };

View File

@@ -1,16 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
css, import { classMap } from "lit/directives/class-map";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@@ -34,11 +27,18 @@ class SearchInput extends LitElement {
this.shadowRoot!.querySelector("paper-input")!.focus(); this.shadowRoot!.querySelector("paper-input")!.focus();
} }
@query("paper-input", true) private _input!: PaperInputElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<style>
.no-underline:not(.focused) {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<paper-input <paper-input
class=${classMap({ "no-underline": this.noUnderline })}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter} .value=${this.filter}
@@ -62,17 +62,6 @@ class SearchInput extends LitElement {
`; `;
} }
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line"
) as HTMLElement).style.display = this.noUnderline ? "none" : "block";
}
}
private async _filterChanged(value: string) { private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) }); fireEvent(this, "value-changed", { value: String(value) });
} }

View File

@@ -1,2 +0,0 @@
export const escapeRegExp = (text: string): string =>
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");

View File

@@ -92,7 +92,7 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos]; return word[pos] !== wordLow[pos];
} }
export function isPatternInWord( function isPatternInWord(
patternLow: string, patternLow: string,
patternPos: number, patternPos: number,
patternLen: number, patternLen: number,
@@ -121,7 +121,7 @@ enum Arrow {
} }
/** /**
* An array representing a fuzzy match. * An array representating a fuzzy match.
* *
* 0. the score * 0. the score
* 1. the offset at which matching started * 1. the offset at which matching started

View File

@@ -5,7 +5,7 @@ import { fuzzyScore } from "./filter";
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier") * in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
* *
* @param {string} filter - Sequence of letters to check for * @param {string} filter - Sequence of letters to check for
* @param {ScorableTextItem} item - Item against whose strings will be checked * @param {string} word - Word to check for sequence
* *
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match. * @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/ */

View File

@@ -1,4 +1,4 @@
import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { FrontendTranslationData, NumberFormat } from "../../data/translation";
/** /**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility. * Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
@@ -9,7 +9,7 @@ import { FrontendLocaleData, NumberFormat } from "../../data/translation";
*/ */
export const formatNumber = ( export const formatNumber = (
num: string | number, num: string | number,
locale?: FrontendLocaleData, locale?: FrontendTranslationData,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): string => { ): string => {
let format: string | string[] | undefined; let format: string | string[] | undefined;

View File

@@ -1,11 +0,0 @@
// https://regex101.com/r/kc5C14/2
const regExpString = "^\\d{4}-(0[1-9]|1[0-2])-([12]\\d|0[1-9]|3[01])";
const regExp = new RegExp(regExpString + "$");
// 2nd expression without the "end of string" enforced, so it can be used
// to just verify the start of a string and then based on that result e.g.
// check for a full timestamp string efficiently.
const regExpNoStringEnd = new RegExp(regExpString);
export const isDate = (input: string, allowCharsAfterDate = false): boolean =>
allowCharsAfterDate ? regExpNoStringEnd.test(input) : regExp.test(input);

View File

@@ -1,11 +0,0 @@
// https://stackoverflow.com/a/14322189/1947205
// Changes:
// 1. Do not allow a plus or minus at the start.
// 2. Enforce that we have a "T" or a blank after the date portion
// to ensure we have a timestamp and not only a date.
// 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

@@ -1,6 +0,0 @@
import { refine, string } from "superstruct";
const isCustomType = (value: string) => value.startsWith("custom:");
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@@ -1,6 +1,11 @@
import { refine, string } from "superstruct"; import { refine, string } from "superstruct";
const isEntityId = (value: string): boolean => value.includes("."); const isEntityId = (value: string): boolean => {
if (!value.includes(".")) {
return false;
}
return true;
};
export const entityId = () => export const entityId = () =>
refine(string(), "entity ID (domain.entity)", isEntityId); refine(string(), "entity ID (domain.entity)", isEntityId);

View File

@@ -1,5 +1,10 @@
import { refine, string } from "superstruct"; import { refine, string } from "superstruct";
const isIcon = (value: string) => value.includes(":"); const isIcon = (value: string) => {
if (!value.includes(":")) {
return false;
}
return true;
};
export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon); export const icon = () => refine(string(), "icon (mdi:icon-name)", isIcon);

View File

@@ -1,4 +1,4 @@
import { shouldPolyfill } from "@formatjs/intl-pluralrules/lib/should-polyfill"; import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types"; import { Resources } from "../../types";
@@ -86,15 +86,11 @@ export const computeLocalize = async (
| undefined; | undefined;
if (!translatedMessage) { if (!translatedMessage) {
try {
translatedMessage = new IntlMessageFormat( translatedMessage = new IntlMessageFormat(
translatedValue, translatedValue,
language, language,
formats formats
); );
} catch (err) {
return "Translation error: " + err.message;
}
cache._localizationCache[messageKey] = translatedMessage; cache._localizationCache[messageKey] = translatedMessage;
} }

View File

@@ -4,25 +4,29 @@
// be triggered. The function will be called after it stops being called for // be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the // N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing. // leading edge, instead of the trailing.
// eslint-disable-next-line: ban-types
export const debounce = <T extends any[]>( export const debounce = <T extends (...args) => unknown>(
func: (...args: T) => void, func: T,
wait: number, wait,
immediate = false immediate = false
) => { ): T => {
let timeout: number | undefined; let timeout;
return (...args: T): void => { // @ts-ignore
return function (...args) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const later = () => { const later = () => {
timeout = undefined; timeout = null;
if (!immediate) { if (!immediate) {
func(...args); func.apply(context, args);
} }
}; };
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
clearTimeout(timeout); clearTimeout(timeout);
timeout = window.setTimeout(later, wait); timeout = setTimeout(later, wait);
if (callNow) { if (callNow) {
func(...args); func.apply(context, args);
} }
}; };
}; };

View File

@@ -1,10 +0,0 @@
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);
}, ms);
});
// Returns a race between our timeout and the passed in promise
return Promise.race([promise, timeout]);
};

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css, css,
@@ -173,8 +173,8 @@ export class HaDataTable extends LitElement {
this.updateComplete.then(() => this._calcTableHeight()); this.updateComplete.then(() => this._calcTableHeight());
} }
public willUpdate(properties: PropertyValues) { protected updated(properties: PropertyValues) {
super.willUpdate(properties); super.updated(properties);
if (properties.has("columns")) { if (properties.has("columns")) {
this._filterable = Object.values(this.columns).some( this._filterable = Object.values(this.columns).some(
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
aria-rowcount=${this._filteredData.length + 1} aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({ style=${styleMap({
height: this.autoHeight height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 53}px` ? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`, : `calc(100% - ${this._headerHeight}px)`,
})} })}
> >
@@ -340,11 +340,8 @@ export class HaDataTable extends LitElement {
${scroll({ ${scroll({
items: this._items, items: this._items,
layout: Layout1d, layout: Layout1d,
// @ts-expect-error
renderItem: (row: DataTableRowData, index) => { renderItem: (row: DataTableRowData, index) => {
// not sure how this happens...
if (!row) {
return html``;
}
if (row.append) { if (row.append) {
return html` return html`
<div class="mdc-data-table__row">${row.content}</div> <div class="mdc-data-table__row">${row.content}</div>
@@ -477,16 +474,15 @@ export class HaDataTable extends LitElement {
} }
if (this.appendRow || this.hasFab) { if (this.appendRow || this.hasFab) {
const items = [...data]; this._items = [...data];
if (this.appendRow) { if (this.appendRow) {
items.push({ append: true, content: this.appendRow }); this._items.push({ append: true, content: this.appendRow });
} }
if (this.hasFab) { if (this.hasFab) {
items.push({ empty: true }); this._items.push({ empty: true });
} }
this._items = items;
} else { } else {
this._items = data; this._items = data;
} }
@@ -919,11 +915,13 @@ export class HaDataTable extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.scroller { .scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px); height: calc(100% - 57px);
} }
.mdc-data-table__table:not(.auto-height) .scroller {
.mdc-data-table__table.auto-height .scroller { overflow: auto;
overflow-y: hidden !important;
} }
.grows { .grows {
flex-grow: 1; flex-grow: 1;

View File

@@ -38,7 +38,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-devices-picker"; import "./ha-devices-picker";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
interface DevicesByArea { interface DevicesByArea {
[areaId: string]: AreaDevices; [areaId: string]: AreaDevices;
@@ -50,7 +49,14 @@ interface AreaDevices {
devices: string[]; devices: string[];
} }
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style> const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: AreaDevices }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
width: 100%; width: 100%;
margin: -10px 0; margin: -10px 0;
@@ -68,10 +74,17 @@ const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
</style> </style>
<paper-item> <paper-item>
<paper-item-body two-line=""> <paper-item-body two-line="">
<div class="name">${item.name}</div> <div class='name'>[[item.name]]</div>
<div secondary>${item.devices.length} devices</div> <div secondary>[[item.devices.length]] devices</div>
</paper-item-body> </paper-item-body>
</paper-item>`; </paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector(
"[secondary]"
)!.textContent = `${model.item.devices.length.toString()} devices`;
};
@customElement("ha-area-devices-picker") @customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@@ -297,7 +310,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
item-label-path="name" item-label-path="name"
.items=${areas} .items=${areas}
.value=${this._value} .value=${this._value}
${comboBoxRenderer(rowRenderer)} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
> >

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceActions, fetchDeviceActions,
localizeDeviceAutomationAction, localizeDeviceAutomationAction,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker") @customElement("ha-device-action-picker")

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceConditions, fetchDeviceConditions,
localizeDeviceAutomationCondition, localizeDeviceAutomationCondition,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-condition-picker") @customElement("ha-device-condition-picker")

View File

@@ -33,7 +33,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
interface Device { interface Device {
name: string; name: string;
@@ -45,18 +44,27 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
) => boolean; ) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style> const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -10px 0; margin: -10px 0;
padding: 0; padding: 0;
} }
</style> </style>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line="">
${item.name} <div class='name'>[[item.name]]</div>
<span secondary>${item.area}</span> <div secondary>[[item.area]]</div>
</paper-item-body> </paper-item-body>
</paper-item>`; </paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector("[secondary]")!.textContent = model.item.area!;
};
@customElement("ha-device-picker") @customElement("ha-device-picker")
export class HaDevicePicker extends SubscribeMixin(LitElement) { export class HaDevicePicker extends SubscribeMixin(LitElement) {

View File

@@ -4,6 +4,7 @@ import {
fetchDeviceTriggers, fetchDeviceTriggers,
localizeDeviceAutomationTrigger, localizeDeviceAutomationTrigger,
} from "../../data/device_automation"; } from "../../data/device_automation";
import "../ha-paper-dropdown-menu";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker") @customElement("ha-device-trigger-picker")

View File

@@ -12,7 +12,6 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
@@ -23,13 +22,22 @@ import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -5px -10px; margin: -10px;
padding: 0; padding: 0;
} }
</style> </style>
<paper-item>${formatAttributeName(item)}</paper-item>`; <paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = formatAttributeName(
model.item
);
};
@customElement("ha-entity-attribute-picker") @customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement { class HaEntityAttributePicker extends LitElement {
@@ -74,8 +82,8 @@ class HaEntityAttributePicker extends LitElement {
<vaadin-combo-box-light <vaadin-combo-box-light
.value=${this._value} .value=${this._value}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
attr-for-value="bind-value" attr-for-value="bind-value"
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >

View File

@@ -13,7 +13,6 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -26,19 +25,32 @@ import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style> const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassEntity }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-icon-item { paper-icon-item {
margin: -10px; margin: -10px;
padding: 0; padding: 0;
} }
</style> </style>
<paper-icon-item> <paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item}></state-badge> <state-badge slot="item-icon"></state-badge>
<paper-item-body two-line=""> <paper-item-body two-line="">
${computeStateName(item)} <div class='name'></div>
<span secondary>${item.entity_id}</span> <div secondary></div>
</paper-item-body> </paper-item-body>
</paper-icon-item>`; </paper-icon-item>
`;
}
root.querySelector("state-badge")!.stateObj = model.item;
root.querySelector(".name")!.textContent = computeStateName(model.item);
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
};
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@@ -209,7 +221,7 @@ export class HaEntityPicker extends LitElement {
item-label-path="entity_id" item-label-path="entity_id"
.value=${this._value} .value=${this._value}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
${comboBoxRenderer(rowRenderer)} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}

View File

@@ -15,7 +15,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { domainIcon } from "../../common/entity/domain_icon"; import { domainIcon } from "../../common/entity/domain_icon";
import { stateIcon } from "../../common/entity/state_icon"; import { stateIcon } from "../../common/entity/state_icon";
import { timerTimeRemaining } from "../../data/timer"; import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
import { formatNumber } from "../../common/string/format_number"; import { formatNumber } from "../../common/string/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";

View File

@@ -9,20 +9,32 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box"; import { HaComboBox } from "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`<style> const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassioAddonInfo }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -10px 0; margin: -10px 0;
padding: 0; padding: 0;
} }
</style> </style>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line="">
${item.name} <div class='name'>[[item.name]]</div>
<span secondary>${item.slug}</span> <div secondary>[[item.slug]]</div>
</paper-item-body> </paper-item-body>
</paper-item>`; </paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.slug;
};
@customElement("ha-addon-picker") @customElement("ha-addon-picker")
class HaAddonPicker extends LitElement { class HaAddonPicker extends LitElement {

View File

@@ -14,9 +14,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
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";
@@ -44,9 +42,14 @@ import { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-svg-icon"; import "./ha-svg-icon";
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( const rowRenderer = (
item root: HTMLElement,
) => html`<style> _owner,
model: { item: AreaRegistryEntry }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -10px 0; margin: -10px 0;
padding: 0; padding: 0;
@@ -55,9 +58,20 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
font-weight: 500; font-weight: 500;
} }
</style> </style>
<paper-item class=${classMap({ "add-new": item.area_id === "add_new" })}> <paper-item>
<paper-item-body two-line>${item.name}</paper-item-body> <paper-item-body two-line>
</paper-item>`; <div class='name'>[[item.name]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
if (model.item.area_id === "add_new") {
root.querySelector("paper-item")!.className = "add-new";
} else {
root.querySelector("paper-item")!.classList.remove("add-new");
}
};
@customElement("ha-area-picker") @customElement("ha-area-picker")
export class HaAreaPicker extends SubscribeMixin(LitElement) { export class HaAreaPicker extends SubscribeMixin(LitElement) {
@@ -326,8 +340,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled} .disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged} @value-changed=${this._areaChanged}
> >

View File

@@ -1,24 +1,20 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import hassAttributeUtil, { import hassAttributeUtil, {
formatAttributeName, formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util"; } from "../util/hass-attributes-util";
import "./ha-expansion-panel";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes") @customElement("ha-attributes")
class HaAttributes extends LitElement { class HaAttributes extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: HassEntity; @property() public stateObj?: HassEntity;
@property({ attribute: "extra-filters" }) public extraFilters?: string; @property({ attribute: "extra-filters" }) public extraFilters?: string;
@state() private _expanded = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.stateObj) { if (!this.stateObj) {
return html``; return html``;
@@ -34,30 +30,16 @@ class HaAttributes extends LitElement {
} }
return html` return html`
<ha-expansion-panel <hr />
.header=${this.hass.localize( <div>
"ui.components.attributes.expansion_header"
)}
outlined
@expanded-will-change=${this.expandedChanged}
>
<div class="attribute-container">
${this._expanded
? html`
${attributes.map( ${attributes.map(
(attribute) => html` (attribute) => html`
<div class="data-entry"> <div class="data-entry">
<div class="key">${formatAttributeName(attribute)}</div> <div class="key">${formatAttributeName(attribute)}</div>
<div class="value"> <div class="value">${this.formatAttribute(attribute)}</div>
${this.formatAttribute(attribute)}
</div>
</div> </div>
` `
)} )}
`
: ""}
</div>
</ha-expansion-panel>
${this.stateObj.attributes.attribution ${this.stateObj.attributes.attribution
? html` ? html`
<div class="attribution"> <div class="attribution">
@@ -65,6 +47,7 @@ class HaAttributes extends LitElement {
</div> </div>
` `
: ""} : ""}
</div>
`; `;
} }
@@ -72,9 +55,6 @@ class HaAttributes extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
.attribute-container {
margin-bottom: 8px;
}
.data-entry { .data-entry {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -91,7 +71,6 @@ class HaAttributes extends LitElement {
.attribution { .attribution {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-align: center; text-align: center;
margin-top: 16px;
} }
pre { pre {
font-family: inherit; font-family: inherit;
@@ -123,11 +102,38 @@ class HaAttributes extends LitElement {
return "-"; return "-";
} }
const value = this.stateObj.attributes[attribute]; const value = this.stateObj.attributes[attribute];
return formatAttributeValue(this.hass, value); return this.formatAttributeValue(value);
} }
private expandedChanged(ev) { private formatAttributeValue(value: any): string | TemplateResult {
this._expanded = ev.detail.expanded; if (value === null) {
return "-";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html` <pre>${until(yaml, "")}</pre> `;
}
// URL handling
if (typeof value === "string" && value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href="${value}"
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
return Array.isArray(value) ? value.join(", ") : value;
} }
} }

View File

@@ -12,8 +12,6 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public fixed = false;
@query("mwc-menu", true) private _menu?: Menu; @query("mwc-menu", true) private _menu?: Menu;
public get items() { public get items() {
@@ -31,7 +29,6 @@ export class HaButtonMenu extends LitElement {
</div> </div>
<mwc-menu <mwc-menu
.corner=${this.corner} .corner=${this.corner}
.fixed=${this.fixed}
.multi=${this.multi} .multi=${this.multi}
.activatable=${this.activatable} .activatable=${this.activatable}
> >

View File

@@ -1,5 +1,4 @@
import { CircularProgress } from "@material/mwc-circular-progress"; import { CircularProgress } from "@material/mwc-circular-progress";
import { CSSResultGroup, css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("ha-circular-progress") @customElement("ha-circular-progress")
@@ -42,17 +41,6 @@ export class HaCircularProgress extends CircularProgress {
public get indeterminate() { public get indeterminate() {
return this.active; return this.active;
} }
static get styles(): CSSResultGroup {
return [
super.styles,
css`
:host {
overflow: hidden;
}
`,
];
}
} }
declare global { declare global {

View File

@@ -6,20 +6,31 @@ import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const defaultRowRenderer = (
root: HTMLElement,
_owner,
model: { item: any }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -5px -10px; margin: -5px -10px;
padding: 0; padding: 0;
} }
</style> </style>
<paper-item>${item}</paper-item>`; <paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-combo-box") @customElement("ha-combo-box")
export class HaComboBox extends LitElement { export class HaComboBox extends LitElement {
@@ -42,7 +53,11 @@ export class HaComboBox extends LitElement {
@property({ attribute: "item-id-path" }) public itemIdPath?: string; @property({ attribute: "item-id-path" }) public itemIdPath?: string;
@property() public renderer?: ComboBoxLitRenderer<any>; @property() public renderer?: (
root: HTMLElement,
owner: HTMLElement,
model: { item: any }
) => void;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@@ -75,9 +90,9 @@ export class HaComboBox extends LitElement {
.value=${this.value} .value=${this.value}
.items=${this.items} .items=${this.items}
.filteredItems=${this.filteredItems} .filteredItems=${this.filteredItems}
.renderer=${this.renderer || defaultRowRenderer}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled} .disabled=${this.disabled}
${comboBoxRenderer(this.renderer || defaultRowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}

View File

@@ -23,8 +23,8 @@ class HaCoverControls extends LitElement {
@state() private _entityObj?: CoverEntity; @state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void { protected updated(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.updated(changedProperties);
if (changedProperties.has("stateObj")) { if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj); this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -22,8 +22,8 @@ class HaCoverTiltControls extends LitElement {
@state() private _entityObj?: CoverEntity; @state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void { protected updated(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.updated(changedProperties);
if (changedProperties.has("stateObj")) { if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj); this._entityObj = new CoverEntity(this.hass, this.stateObj);

View File

@@ -14,7 +14,6 @@ import {
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../common/datetime/format_date_time"; import { formatDateTime } from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./date-range-picker"; import "./date-range-picker";
@@ -44,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) { if (!oldHass || oldHass.locale !== this.hass.locale) {
this._hour24format = !useAmPm(this.hass.locale); this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass); this._rtlDirection = computeRTLDirection(this.hass);
} }
} }
@@ -107,6 +106,16 @@ export class HaDateRangePicker extends LitElement {
`; `;
} }
private _compute24hourFormat() {
return (
new Intl.DateTimeFormat(this.hass.language, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")!.value.length === 2
);
}
private _setDateRange(ev: CustomEvent<ActionDetail>) { private _setDateRange(ev: CustomEvent<ActionDetail>) {
const dateRange = Object.values(this.ranges!)[ev.detail.index]; const dateRange = Object.values(this.ranges!)[ev.detail.index];
const dateRangePicker = this._dateRangePicker; const dateRangePicker = this._dateRangePicker;

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-expansion-panel") @customElement("ha-expansion-panel")
@@ -14,17 +13,12 @@ class HaExpansionPanel extends LitElement {
@property() header?: string; @property() header?: string;
@property() secondary?: string;
@query(".container") private _container!: HTMLDivElement; @query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="summary" @click=${this._toggleContainer}> <div class="summary" @click=${this._toggleContainer}>
<slot class="header" name="header"> <slot name="header">${this.header}</slot>
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</slot>
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}" class="summary-icon ${classMap({ expanded: this.expanded })}"
@@ -43,25 +37,17 @@ class HaExpansionPanel extends LitElement {
this._container.style.removeProperty("height"); this._container.style.removeProperty("height");
} }
private async _toggleContainer(): Promise<void> { private _toggleContainer(): void {
const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
if (newExpanded) {
// allow for dynamic content to be rendered
await nextRender();
}
const scrollHeight = this._container.scrollHeight; const scrollHeight = this._container.scrollHeight;
this._container.style.height = `${scrollHeight}px`; this._container.style.height = `${scrollHeight}px`;
if (!newExpanded) { if (this.expanded) {
setTimeout(() => { setTimeout(() => {
this._container.style.height = "0px"; this._container.style.height = "0px";
}, 0); }, 0);
} }
this.expanded = newExpanded; this.expanded = !this.expanded;
fireEvent(this, "expanded-changed", { expanded: this.expanded }); fireEvent(this, "expanded-changed", { expanded: this.expanded });
} }
@@ -111,16 +97,6 @@ class HaExpansionPanel extends LitElement {
.container.expanded { .container.expanded {
height: auto; height: auto;
} }
.header {
display: block;
}
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: 12px;
}
`; `;
} }
} }
@@ -135,8 +111,5 @@ declare global {
"expanded-changed": { "expanded-changed": {
expanded: boolean; expanded: boolean;
}; };
"expanded-will-change": {
expanded: boolean;
};
} }
} }

View File

@@ -4,17 +4,17 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { formatNumber } from "../common/string/format_number"; import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status"; import { afterNextRender } from "../common/util/render-status";
import { FrontendLocaleData } from "../data/translation"; import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
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 isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge") @customElement("ha-gauge")
export class Gauge extends LitElement { export class Gauge extends LitElement {
@property({ type: Number }) public min = 0; @property({ type: Number }) public min = 0;
@@ -23,7 +23,7 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0; @property({ type: Number }) public value = 0;
@property() public locale!: FrontendLocaleData; @property() public locale!: FrontendTranslationData;
@property() public label = ""; @property() public label = "";

View File

@@ -13,11 +13,6 @@ import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
type HlsLite = Omit<
HlsType,
"subtitleTrackController" | "audioTrackController" | "emeController"
>;
@customElement("ha-hls-player") @customElement("ha-hls-player")
class HaHLSPlayer extends LitElement { class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -44,7 +39,7 @@ class HaHLSPlayer extends LitElement {
@state() private _attached = false; @state() private _attached = false;
private _hlsPolyfillInstance?: HlsLite; private _hlsPolyfillInstance?: HlsType;
private _useExoPlayer = false; private _useExoPlayer = false;
@@ -108,8 +103,7 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js")) const Hls = (await import("hls.js")).default;
.default;
let hlsSupported = Hls.isSupported(); let hlsSupported = Hls.isSupported();
if (!hlsSupported) { if (!hlsSupported) {
@@ -188,7 +182,7 @@ class HaHLSPlayer extends LitElement {
url: string url: string
) { ) {
const hls = new Hls({ const hls = new Hls({
backBufferLength: 60, liveBackBufferLength: 60,
fragLoadingTimeOut: 30000, fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000, manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000, levelLoadingTimeOut: 30000,

View File

@@ -125,7 +125,6 @@ export class HaIcon extends LitElement {
databaseIcon = await getIcon(iconName); databaseIcon = await getIcon(iconName);
} catch (_err) { } catch (_err) {
// Firefox in private mode doesn't support IDB // Firefox in private mode doesn't support IDB
// iOS Safari sometimes doesn't open the DB
databaseIcon = undefined; databaseIcon = undefined;
} }

View File

@@ -1,179 +0,0 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultGroup,
html,
nothing,
LitElement,
TemplateResult,
} from "lit";
import { customElement, state, property } from "lit/decorators";
import {
Adapter,
NetworkConfig,
IPv6ConfiguredAddress,
IPv4ConfiguredAddress,
} from "../data/network";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
import "./ha-icon";
const format_addresses = (
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
): TemplateResult =>
html`${addresses.map((address, i) => [
html`<span>${address.address}/${address.network_prefix}</span>`,
i < addresses.length - 1 ? ", " : nothing,
])}`;
const format_auto_detected_interfaces = (
adapters: Adapter[]
): Array<TemplateResult | string> =>
adapters.map((adapter) =>
adapter.auto
? html`${adapter.name}
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
: ""
);
declare global {
interface HASSDomEvents {
"network-config-changed": { configured_adapters: string[] };
}
}
@customElement("ha-network")
export class HaNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public networkConfig?: NetworkConfig;
@state() private _expanded?: boolean;
protected render(): TemplateResult {
if (this.networkConfig === undefined) {
return html``;
}
const configured_adapters = this.networkConfig.configured_adapters || [];
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id="auto_configure"
@change=${this._handleAutoConfigureCheckboxClick}
.checked=${!configured_adapters.length}
name="auto_configure"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
<span slot="description" data-for="auto_configure">
Detected:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</span>
</ha-settings-row>
${configured_adapters.length || this._expanded
? this.networkConfig.adapters.map(
(adapter) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
id=${adapter.name}
@change=${this._handleAdapterCheckboxClick}
.checked=${configured_adapters.includes(adapter.name)}
.adapter=${adapter.name}
name=${adapter.name}
>
</ha-checkbox>
</span>
<span slot="heading">
Adapter: ${adapter.name}
${adapter.default
? html`<ha-icon .icon="hass:star"></ha-icon> (Default)`
: ""}
</span>
<span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
</span>
</ha-settings-row>`
)
: ""}
`;
}
private _handleAutoConfigureCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
if (this.networkConfig === undefined) {
return;
}
let configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
this._expanded = false;
configured_adapters = [];
} else {
this._expanded = true;
for (const adapter of this.networkConfig.adapters) {
if (adapter.default) {
configured_adapters = [adapter.name];
break;
}
}
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
private _handleAdapterCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const adapter_name = (checkbox as any).name;
if (this.networkConfig === undefined) {
return;
}
const configured_adapters = [...this.networkConfig.configured_adapters];
if (checkbox.checked) {
configured_adapters.push(adapter_name);
} else {
const index = configured_adapters.indexOf(adapter_name, 0);
configured_adapters.splice(index, 1);
}
fireEvent(this, "network-config-changed", {
configured_adapters: configured_adapters,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
span[slot="heading"],
span[slot="description"] {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-network": HaNetwork;
}
}

View File

@@ -1,13 +1,10 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { TimeSelector } from "../../data/selector"; import { TimeSelector } from "../../data/selector";
import { FrontendLocaleData } from "../../data/translation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../paper-time-input"; import "../paper-time-input";
@customElement("ha-selector-time") @customElement("ha-selector-time")
export class HaTimeSelector extends LitElement { export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@@ -20,12 +17,13 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean => private _useAmPm = memoizeOne((language: string) => {
useAmPm(locale) const test = new Date().toLocaleString(language);
); return test.includes("AM") || test.includes("PM");
});
protected render() { protected render() {
const useAMPM = this._useAmPmMem(this.hass.locale); const useAMPM = this._useAmPm(this.hass.locale.language);
const parts = this.value?.split(":") || []; const parts = this.value?.split(":") || [];
const hours = parts[0]; const hours = parts[0];
@@ -50,7 +48,7 @@ export class HaTimeSelector extends LitElement {
private _timeChanged(ev) { private _timeChanged(ev) {
let value = ev.target.value; let value = ev.target.value;
const useAMPM = this._useAmPmMem(this.hass.locale); const useAMPM = this._useAmPm(this.hass.locale.language);
let hours = Number(ev.target.hour || 0); let hours = Number(ev.target.hour || 0);
if (value && useAMPM) { if (value && useAMPM) {
if (ev.target.amPm === "PM") { if (ev.target.amPm === "PM") {

View File

@@ -6,22 +6,33 @@ import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration"; import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-combo-box"; import "./ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = ( const rowRenderer = (
item root: HTMLElement,
) => html`<style> _owner,
model: { item: { service: string; name: string } }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item { paper-item {
margin: -10px 0; margin: -10px 0;
padding: 0; padding: 0;
} }
</style> </style>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line="">
${item.name} <div class='name'>[[item.name]]</div>
<span secondary>${item.name === item.service ? "" : item.service}</span> <div secondary>[[item.service]]</div>
</paper-item-body> </paper-item-body>
</paper-item>`; </paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent =
model.item.name === model.item.service ? "" : model.item.service;
};
class HaServicePicker extends LitElement { class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -117,10 +117,7 @@ export class HaTab extends LitElement {
} }
:host([narrow]) { :host([narrow]) {
min-width: 0; padding: 0 16px;
display: flex;
justify-content: center;
overflow: hidden;
} }
`; `;
} }

View File

@@ -1,4 +1,4 @@
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml"; import { safeDump, safeLoad } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -20,8 +20,6 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
export class HaYamlEditor extends LitElement { export class HaYamlEditor extends LitElement {
@property() public value?: any; @property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@property() public defaultValue?: any; @property() public defaultValue?: any;
@property() public isValid = true; @property() public isValid = true;
@@ -32,10 +30,7 @@ export class HaYamlEditor extends LitElement {
public setValue(value): void { public setValue(value): void {
try { try {
this._yaml = this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema })
: "";
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err, value); console.error(err, value);
@@ -72,7 +67,7 @@ export class HaYamlEditor extends LitElement {
if (this._yaml) { if (this._yaml) {
try { try {
parsed = load(this._yaml, { schema: this.yamlSchema }); parsed = safeLoad(this._yaml);
} catch (err) { } catch (err) {
// Invalid YAML // Invalid YAML
isValid = false; isValid = false;

View File

@@ -1,69 +0,0 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@property({ attribute: "entity-picture" }) public entityPicture?: string;
@property({ attribute: "entity-color" }) public entityColor?: string;
protected render() {
return html`
<div
class="marker"
style=${styleMap({ "border-color": this.entityColor })}
@click=${this._badgeTap}
>
${this.entityPicture
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
</div>
`;
}
private _badgeTap(ev: Event) {
ev.stopPropagation();
if (this.entityId) {
fireEvent(this, "hass-more-info", { entityId: this.entityId });
}
}
static get styles() {
return css`
.marker {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
overflow: hidden;
width: 48px;
height: 48px;
font-size: var(--ha-marker-font-size, 1.5em);
border-radius: 50%;
border: 1px solid var(--ha-marker-color, var(--primary-color));
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.entity-picture {
background-size: cover;
height: 100%;
width: 100%;
}
`;
}
}
customElements.define("ha-entity-marker", HaEntityMarker);

View File

@@ -0,0 +1,299 @@
import {
Circle,
DivIcon,
DragEndEvent,
LatLng,
LeafletMouseEvent,
Map,
Marker,
TileLayer,
} from "leaflet";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor")
class LocationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public location?: [number, number];
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string;
@property() public icon?: string;
@property({ type: Boolean }) public darkMode?: boolean;
public fitZoom = 16;
private _iconEl?: DivIcon;
private _ignoreFitToMap?: [number, number];
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle;
public fitMap(): void {
if (!this._leafletMap || !this.location) {
return;
}
if (this._locationMarker && "getBounds" in this._locationMarker) {
this._leafletMap.fitBounds(this._locationMarker.getBounds());
} else {
this._leafletMap.setView(this.location, this.fitZoom);
}
this._ignoreFitToMap = this.location;
}
protected render(): TemplateResult {
return html` <div id="map"></div> `;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initMap();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
// Still loading.
if (!this.Leaflet) {
return;
}
if (changedProps.has("location")) {
this._updateMarker();
if (
this.location &&
(!this._ignoreFitToMap ||
this._ignoreFitToMap[0] !== this.location[0] ||
this._ignoreFitToMap[1] !== this.location[1])
) {
this.fitMap();
}
}
if (changedProps.has("radius")) {
this._updateRadius();
}
if (changedProps.has("radiusColor")) {
this._updateRadiusColor();
}
if (changedProps.has("icon")) {
this._updateIcon();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes?.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes?.darkMode,
Boolean(this.radius)
);
this._leafletMap.addEventListener(
"click",
// @ts-ignore
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
);
this._updateIcon();
this._updateMarker();
this.fitMap();
this._leafletMap.invalidateSize();
}
private _locationUpdated(latlng: LatLng) {
let longitude = latlng.lng;
if (Math.abs(longitude) > 180.0) {
// Normalize longitude if map provides values beyond -180 to +180 degrees.
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
}
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
fireEvent(this, "change", undefined, { bubbles: false });
}
private _radiusUpdated() {
this._ignoreFitToMap = this.location;
this.radius = (this._locationMarker as Circle).getRadius();
fireEvent(this, "change", undefined, { bubbles: false });
}
private _updateIcon() {
if (!this.icon) {
this._iconEl = undefined;
return;
}
// create icon
let iconHTML = "";
const el = document.createElement("ha-icon");
el.setAttribute("icon", this.icon);
iconHTML = el.outerHTML;
this._iconEl = this.Leaflet!.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "light leaflet-edit-move",
});
this._setIcon();
}
private _setIcon() {
if (!this._locationMarker || !this._iconEl) {
return;
}
if (!this.radius) {
(this._locationMarker as Marker).setIcon(this._iconEl);
return;
}
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
moveMarker.setIcon(this._iconEl);
}
private _setupEdit() {
// @ts-ignore
this._locationMarker.editing.enable();
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
// @ts-ignore
const resizeMarker = this._locationMarker.editing._resizeMarkers[0];
this._setIcon();
moveMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._radiusUpdated(ev)
);
}
private async _updateMarker(): Promise<void> {
if (!this.location) {
if (this._locationMarker) {
this._locationMarker.remove();
this._locationMarker = undefined;
}
return;
}
if (this._locationMarker) {
this._locationMarker.setLatLng(this.location);
if (this.radius) {
// @ts-ignore
this._locationMarker.editing.disable();
await nextRender();
this._setupEdit();
}
return;
}
if (!this.radius) {
this._locationMarker = this.Leaflet!.marker(this.location, {
draggable: true,
});
this._setIcon();
this._locationMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
this._leafletMap!.addLayer(this._locationMarker);
} else {
this._locationMarker = this.Leaflet!.circle(this.location, {
color: this.radiusColor || defaultRadiusColor,
radius: this.radius,
});
this._leafletMap!.addLayer(this._locationMarker);
this._setupEdit();
}
}
private _updateRadius(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setRadius(this.radius);
}
private _updateRadiusColor(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setStyle({ color: this.radiusColor });
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
height: 300px;
}
#map {
height: 100%;
background: inherit;
}
.leaflet-edit-move {
border-radius: 50%;
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-location-editor": LocationEditor;
}
}

View File

@@ -3,8 +3,10 @@ import {
DivIcon, DivIcon,
DragEndEvent, DragEndEvent,
LatLng, LatLng,
Map,
Marker, Marker,
MarkerOptions, MarkerOptions,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@@ -14,13 +16,15 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map"; import {
import type { HomeAssistant } from "../../types"; LeafletModuleType,
import "./ha-map"; replaceTileLayer,
import type { HaMap } from "./ha-map"; setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
declare global { declare global {
// for fire event // for fire event
@@ -47,40 +51,38 @@ export interface MarkerLocation {
export class HaLocationsEditor extends LitElement { export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public locations?: MarkerLocation[]; @property() public locations?: MarkerLocation[];
@property({ type: Boolean }) public autoFit = false; public fitZoom = 16;
@property({ type: Number }) public zoom = 16;
@property({ type: Boolean }) public darkMode?: boolean;
@state() private _locationMarkers?: Record<string, Marker | Circle>;
@state() private _circles: Record<string, Circle> = {};
@query("ha-map", true) private map!: HaMap;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType; private Leaflet?: LeafletModuleType;
constructor() { // eslint-disable-next-line
super(); private _leafletMap?: Map;
import("leaflet").then((module) => { private _tileLayer?: TileLayer;
import("leaflet-draw").then(() => {
this.Leaflet = module.default as LeafletModuleType; private _locationMarkers?: { [key: string]: Marker | Circle };
this._updateMarkers();
this.updateComplete.then(() => this.fitMap()); private _circles: Record<string, Circle> = {};
});
});
}
public fitMap(): void { public fitMap(): void {
this.map.fitMap(); if (
!this._leafletMap ||
!this._locationMarkers ||
!Object.keys(this._locationMarkers).length
) {
return;
}
const bounds = this.Leaflet!.latLngBounds(
Object.values(this._locationMarkers).map((item) => item.getLatLng())
);
this._leafletMap.fitBounds(bounds.pad(0.5));
} }
public fitMarker(id: string): void { public fitMarker(id: string): void {
if (!this.map.leafletMap || !this._locationMarkers) { if (!this._leafletMap || !this._locationMarkers) {
return; return;
} }
const marker = this._locationMarkers[id]; const marker = this._locationMarkers[id];
@@ -88,44 +90,29 @@ export class HaLocationsEditor extends LitElement {
return; return;
} }
if ("getBounds" in marker) { if ("getBounds" in marker) {
this.map.leafletMap.fitBounds(marker.getBounds()); this._leafletMap.fitBounds(marker.getBounds());
(marker as Circle).bringToFront(); (marker as Circle).bringToFront();
} else { } else {
const circle = this._circles[id]; const circle = this._circles[id];
if (circle) { if (circle) {
this.map.leafletMap.fitBounds(circle.getBounds()); this._leafletMap.fitBounds(circle.getBounds());
} else { } else {
this.map.leafletMap.setView(marker.getLatLng(), this.zoom); this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
} }
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<ha-map return html` <div id="map"></div> `;
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
} }
private _getLayers = memoizeOne( protected firstUpdated(changedProps: PropertyValues): void {
( super.firstUpdated(changedProps);
circles: Record<string, Circle>, this._initMap();
markers?: Record<string, Marker | Circle>
): Array<Marker | Circle> => {
const layers: Array<Marker | Circle> = [];
Array.prototype.push.apply(layers, Object.values(circles));
if (markers) {
Array.prototype.push.apply(layers, Object.values(markers));
} }
return layers;
}
);
public willUpdate(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.updated(changedProps);
// Still loading. // Still loading.
if (!this.Leaflet) { if (!this.Leaflet) {
@@ -135,6 +122,37 @@ 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 {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.hass.themes.darkMode,
true
);
this._updateMarkers();
this.fitMap();
this._leafletMap.invalidateSize();
} }
private _updateLocation(ev: DragEndEvent) { private _updateLocation(ev: DragEndEvent) {
@@ -171,18 +189,21 @@ export class HaLocationsEditor extends LitElement {
} }
private _updateMarkers(): void { private _updateMarkers(): void {
if (!this.locations || !this.locations.length) { if (this._locationMarkers) {
this._circles = {}; Object.values(this._locationMarkers).forEach((marker) => {
marker.remove();
});
this._locationMarkers = undefined; this._locationMarkers = undefined;
Object.values(this._circles).forEach((circle) => circle.remove());
this._circles = {};
}
if (!this.locations || !this.locations.length) {
return; return;
} }
const locationMarkers = {}; this._locationMarkers = {};
const circles = {};
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
"--accent-color"
);
this.locations.forEach((location: MarkerLocation) => { this.locations.forEach((location: MarkerLocation) => {
let icon: DivIcon | undefined; let icon: DivIcon | undefined;
@@ -207,14 +228,14 @@ export class HaLocationsEditor extends LitElement {
const circle = this.Leaflet!.circle( const circle = this.Leaflet!.circle(
[location.latitude, location.longitude], [location.latitude, location.longitude],
{ {
color: location.radius_color || defaultZoneRadiusColor, color: location.radius_color || defaultRadiusColor,
radius: location.radius, radius: location.radius,
} }
); );
circle.addTo(this._leafletMap!);
if (location.radius_editable || location.location_editable) { if (location.radius_editable || location.location_editable) {
// @ts-ignore // @ts-ignore
circle.editing.enable(); circle.editing.enable();
circle.addEventListener("add", () => {
// @ts-ignore // @ts-ignore
const moveMarker = circle.editing._moveMarker; const moveMarker = circle.editing._moveMarker;
// @ts-ignore // @ts-ignore
@@ -243,10 +264,9 @@ export class HaLocationsEditor extends LitElement {
} else { } else {
resizeMarker.remove(); resizeMarker.remove();
} }
}); this._locationMarkers![location.id] = circle;
locationMarkers[location.id] = circle;
} else { } else {
circles[location.id] = circle; this._circles[location.id] = circle;
} }
} }
if ( if (
@@ -255,7 +275,6 @@ export class HaLocationsEditor extends LitElement {
) { ) {
const options: MarkerOptions = { const options: MarkerOptions = {
title: location.name, title: location.name,
draggable: location.location_editable,
}; };
if (icon) { if (icon) {
@@ -274,14 +293,13 @@ export class HaLocationsEditor extends LitElement {
"click", "click",
// @ts-ignore // @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev) (ev: MouseEvent) => this._markerClicked(ev)
); )
.addTo(this._leafletMap!);
(marker as any).id = location.id; (marker as any).id = location.id;
locationMarkers[location.id] = marker; this._locationMarkers![location.id] = marker;
} }
}); });
this._circles = circles;
this._locationMarkers = locationMarkers;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -290,9 +308,23 @@ export class HaLocationsEditor extends LitElement {
display: block; display: block;
height: 300px; height: 300px;
} }
ha-map { #map {
height: 100%; height: 100%;
} }
.leaflet-marker-draggable {
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
`; `;
} }
} }

View File

@@ -1,15 +1,13 @@
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
import { import {
Circle, css,
CircleMarker, CSSResultGroup,
LatLngTuple, html,
Layer, LitElement,
Map, PropertyValues,
Marker, TemplateResult,
Polyline, } from "lit";
TileLayer, import { customElement, property } from "lit/decorators";
} from "leaflet";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { import {
LeafletModuleType, LeafletModuleType,
replaceTileLayer, replaceTileLayer,
@@ -17,324 +15,194 @@ import {
} 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";
import "./ha-entity-marker"; import { debounce } from "../../common/util/debounce";
import "../../panels/map/ha-entity-marker";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-icon-button";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
export interface HaMapPaths {
points: LatLngTuple[];
color?: string;
gradualOpacity?: number;
}
export interface HaMapEntity {
entity_id: string;
color: string;
}
@customElement("ha-map") @customElement("ha-map")
export class HaMap extends ReactiveElement { class HaMap extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entities?: string[] | HaMapEntity[]; @property() public entities?: string[];
@property({ attribute: false }) public paths?: HaMapPaths[]; @property() public darkMode?: boolean;
@property({ attribute: false }) public layers?: Layer[]; @property() public zoom?: number;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Boolean }) public fitZones?: boolean;
@property({ type: Boolean }) public darkMode?: boolean;
@property({ type: Number }) public zoom = 14;
@state() private _loaded = false;
public leafletMap?: Map;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType; private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer; private _tileLayer?: TileLayer;
// @ts-ignore
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
100,
false
);
private _mapItems: Array<Marker | Circle> = []; private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = []; private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = []; private _connected = false;
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this._loadMap(); this._connected = true;
if (this.hasUpdated) {
this.loadMap();
this._attachObserver(); this._attachObserver();
} }
}
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.leafletMap) { this._connected = false;
this.leafletMap.remove();
this.leafletMap = undefined; if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
this.Leaflet = undefined; this.Leaflet = undefined;
} }
this._loaded = false;
if (this._resizeObserver) { if (this._resizeObserver) {
this._resizeObserver.unobserve(this); this._resizeObserver.unobserve(this._mapEl);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
} }
} }
protected update(changedProps: PropertyValues) { protected render(): TemplateResult {
super.update(changedProps); if (!this.entities) {
return html``;
if (!this._loaded) {
return;
} }
return html` <div id="map"></div> `;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.loadMap();
if (this._connected) {
this._attachObserver();
}
}
protected shouldUpdate(changedProps) {
if (!changedProps.has("hass") || changedProps.size > 1) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (changedProps.has("_loaded") || changedProps.has("entities")) { if (!oldHass || !this.entities) {
this._drawEntities(); return true;
} else if (this._loaded && oldHass && this.entities) { }
// Check if any state has changed // Check if any state has changed
for (const entity of this.entities) { for (const entity of this.entities) {
if ( if (oldHass.states[entity] !== this.hass!.states[entity]) {
oldHass.states[getEntityId(entity)] !== return true;
this.hass!.states[getEntityId(entity)] }
) { }
return false;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass")) {
this._drawEntities(); this._drawEntities();
break; this._fitMap();
}
}
}
if (changedProps.has("_loaded") || changedProps.has("paths")) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
this._drawPaths(); if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
} return;
}
if (changedProps.has("_loaded") || changedProps.has("layers")) { if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
this._drawLayers(changedProps.get("layers") as Layer[] | undefined);
}
if (
changedProps.has("_loaded") ||
((changedProps.has("entities") || changedProps.has("layers")) &&
this.autoFit)
) {
this.fitMap();
}
if (changedProps.has("zoom")) {
this.leafletMap!.setZoom(this.zoom);
}
if (
!changedProps.has("darkMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode))
) {
return; return;
} }
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
this._tileLayer = replaceTileLayer( this._tileLayer = replaceTileLayer(
this.Leaflet!, this.Leaflet,
this.leafletMap!, this._leafletMap,
this._tileLayer!, this._tileLayer,
darkMode this.hass.themes.darkMode
); );
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); }
} }
private async _loadMap(): Promise<void> { private get _mapEl(): HTMLDivElement {
let map = this.shadowRoot!.getElementById("map"); return this.shadowRoot!.getElementById("map") as HTMLDivElement;
if (!map) {
map = document.createElement("div");
map.id = "map";
this.shadowRoot!.append(map);
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
map,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
this._loaded = true;
} }
public fitMap(): void { private async loadMap(): Promise<void> {
if (!this.leafletMap || !this.Leaflet || !this.hass) { [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this.hass) {
return; return;
} }
if (this._mapItems.length === 0) {
if (!this._mapItems.length && !this.layers?.length) { this._leafletMap.setView(
this.leafletMap.setView(
new this.Leaflet.LatLng( new this.Leaflet.LatLng(
this.hass.config.latitude, this.hass.config.latitude,
this.hass.config.longitude this.hass.config.longitude
), ),
this.zoom this.zoom || 14
); );
return; return;
} }
let bounds = this.Leaflet.latLngBounds( const bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : [] this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
); );
this._leafletMap.fitBounds(bounds.pad(0.5));
if (this.fitZones) { if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
this._mapZones?.forEach((zone) => { this._leafletMap.setZoom(this.zoom);
bounds.extend(
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
);
});
} }
this.layers?.forEach((layer: any) => {
bounds.extend(
"getBounds" in layer ? layer.getBounds() : layer.getLatLng()
);
});
if (!this.layers) {
bounds = bounds.pad(0.5);
}
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
}
private _drawLayers(prevLayers: Layer[] | undefined): void {
if (prevLayers) {
prevLayers.forEach((layer) => layer.remove());
}
if (!this.layers) {
return;
}
const map = this.leafletMap!;
this.layers.forEach((layer) => {
map.addLayer(layer);
});
}
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapPaths.length) {
this._mapPaths.forEach((marker) => marker.remove());
this._mapPaths = [];
}
if (!this.paths) {
return;
}
const darkPrimaryColor = getComputedStyle(this).getPropertyValue(
"--dark-primary-color"
);
this.paths.forEach((path) => {
let opacityStep: number;
let baseOpacity: number;
if (path.gradualOpacity) {
opacityStep = path.gradualOpacity / (path.points.length - 2);
baseOpacity = 1 - path.gradualOpacity;
}
for (
let pointIndex = 0;
pointIndex < path.points.length - 1;
pointIndex++
) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
// DRAW line between this and next point
this._mapPaths.push(
Leaflet!.polyline(
[path.points[pointIndex], path.points[pointIndex + 1]],
{
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
}
)
);
}
const pointIndex = path.points.length - 1;
if (pointIndex >= 0) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW end path point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
}
this._mapPaths.forEach((marker) => map.addLayer(marker));
});
} }
private _drawEntities(): void { private _drawEntities(): void {
const hass = this.hass; const hass = this.hass;
const map = this.leafletMap; const map = this._leafletMap;
const Leaflet = this.Leaflet; const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) { if (!hass || !map || !Leaflet) {
return; return;
} }
if (this._mapItems.length) { if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove()); this._mapItems.forEach((marker) => marker.remove());
this._mapItems = [];
} }
const mapItems: Layer[] = (this._mapItems = []);
if (this._mapZones.length) { if (this._mapZones) {
this._mapZones.forEach((marker) => marker.remove()); this._mapZones.forEach((marker) => marker.remove());
this._mapZones = [];
} }
const mapZones: Layer[] = (this._mapZones = []);
if (!this.entities) { const allEntities = this.entities!.concat();
return;
}
const computedStyles = getComputedStyle(this); for (const entity of allEntities) {
const zoneColor = computedStyles.getPropertyValue("--accent-color"); const entityId = entity;
const darkPrimaryColor = computedStyles.getPropertyValue( const stateObj = hass.states[entityId];
"--dark-primary-color"
);
const className =
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
if (!stateObj) { if (!stateObj) {
continue; continue;
} }
@@ -372,12 +240,13 @@ export class HaMap extends ReactiveElement {
} }
// create marker with the icon // create marker with the icon
this._mapZones.push( mapZones.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className, className:
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
}), }),
interactive: false, interactive: false,
title, title,
@@ -385,10 +254,10 @@ export class HaMap extends ReactiveElement {
); );
// create circle around it // create circle around it
this._mapZones.push( mapZones.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: zoneColor, color: "#FF9800",
radius, radius,
}) })
); );
@@ -404,20 +273,17 @@ export class HaMap extends ReactiveElement {
.join("") .join("")
.substr(0, 3); .substr(0, 3);
// create marker with the icon // create market with the icon
this._mapItems.push( mapItems.push(
Leaflet.marker([latitude, longitude], { Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: ` html: `
<ha-entity-marker <ha-entity-marker
entity-id="${getEntityId(entity)}" entity-id="${entityId}"
entity-name="${entityName}" entity-name="${entityName}"
entity-picture="${entityPicture || ""}" entity-picture="${entityPicture || ""}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`
: ""
}
></ha-entity-marker> ></ha-entity-marker>
`, `,
iconSize: [48, 48], iconSize: [48, 48],
@@ -429,10 +295,10 @@ export class HaMap extends ReactiveElement {
// create circle around if entity has accuracy // create circle around if entity has accuracy
if (gpsAccuracy) { if (gpsAccuracy) {
this._mapItems.push( mapItems.push(
Leaflet.circle([latitude, longitude], { Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: darkPrimaryColor, color: "#0288D1",
radius: gpsAccuracy, radius: gpsAccuracy,
}) })
); );
@@ -443,14 +309,20 @@ export class HaMap extends ReactiveElement {
this._mapZones.forEach((marker) => map.addLayer(marker)); this._mapZones.forEach((marker) => map.addLayer(marker));
} }
private async _attachObserver(): Promise<void> { private _attachObserver(): void {
if (!this._resizeObserver) { // Observe changes to map size and invalidate to prevent broken rendering
await installResizeObserver(); // Uses ResizeObserver in Chrome, otherwise window resize event
this._resizeObserver = new ResizeObserver(() => {
this.leafletMap?.invalidateSize({ debounceMoveend: true }); // @ts-ignore
}); if (typeof ResizeObserver === "function") {
// @ts-ignore
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this._mapEl);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
} }
this._resizeObserver.observe(this);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -465,25 +337,13 @@ export class HaMap extends ReactiveElement {
#map.dark { #map.dark {
background: #090909; background: #090909;
} }
.light {
color: #000000;
}
.dark { .dark {
color: #ffffff; color: #ffffff;
} }
.leaflet-marker-draggable {
cursor: move !important; .light {
} color: #000000;
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
} }
`; `;
} }

View File

@@ -45,6 +45,7 @@ import "../ha-button-menu";
import "../ha-card"; import "../ha-card";
import "../ha-circular-progress"; import "../ha-circular-progress";
import "../ha-fab"; import "../ha-fab";
import "../ha-paper-dropdown-menu";
import "../ha-svg-icon"; import "../ha-svg-icon";
declare global { declare global {

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