mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-05 19:26:33 +00:00
Compare commits
58 Commits
exclude-vi
...
checkbox-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0bfeb22209 | ||
![]() |
0e3eed0563 | ||
![]() |
1b1676cecc | ||
![]() |
d911fe6a0b | ||
![]() |
22253a3385 | ||
![]() |
38640c99e3 | ||
![]() |
d6df8bddea | ||
![]() |
ddfc4bd98e | ||
![]() |
3d6674325c | ||
![]() |
194829f5b1 | ||
![]() |
11a77253f4 | ||
![]() |
67be2343f8 | ||
![]() |
e9b1b3d853 | ||
![]() |
8a33d174d7 | ||
![]() |
226d6216b7 | ||
![]() |
1925bb01be | ||
![]() |
82a4806e01 | ||
![]() |
ce419fae7b | ||
![]() |
c68b76e2da | ||
![]() |
342020b420 | ||
![]() |
1e6e99e3c7 | ||
![]() |
2e9aafc377 | ||
![]() |
299c863f49 | ||
![]() |
c2792a28ba | ||
![]() |
635a027a8e | ||
![]() |
a45b8ca8e7 | ||
![]() |
1e6e945a07 | ||
![]() |
f71157c24d | ||
![]() |
e87a2b36cf | ||
![]() |
5418474f64 | ||
![]() |
8836ba6ceb | ||
![]() |
509c5b497a | ||
![]() |
e00bcc9f48 | ||
![]() |
bdef9fd040 | ||
![]() |
c956491ec5 | ||
![]() |
68bc549d6a | ||
![]() |
9c64eafc21 | ||
![]() |
b05e86d442 | ||
![]() |
fe5f9576c6 | ||
![]() |
1b282b65b7 | ||
![]() |
e49664bad3 | ||
![]() |
2a30b55a43 | ||
![]() |
9d0b20adce | ||
![]() |
acd5e1c081 | ||
![]() |
cc1c5e45b2 | ||
![]() |
038199c447 | ||
![]() |
8a1eab7ceb | ||
![]() |
bc5bd35448 | ||
![]() |
1795fd56b7 | ||
![]() |
4a7c33edad | ||
![]() |
797f60d725 | ||
![]() |
2427d68aa1 | ||
![]() |
00c6b0f8ed | ||
![]() |
7b8d4ab3d6 | ||
![]() |
07a1a805f6 | ||
![]() |
d8bab6aba9 | ||
![]() |
a930e2dc75 | ||
![]() |
2eb35668fa |
@@ -104,5 +104,6 @@
|
|||||||
"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/*"]
|
||||||
}
|
}
|
||||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -6,7 +6,6 @@ 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
|
||||||
|
|
||||||
@@ -64,6 +63,9 @@ 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
|
||||||
@@ -73,7 +75,7 @@ jobs:
|
|||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@master
|
uses: home-assistant/wheels@master
|
||||||
with:
|
with:
|
||||||
tag: ${{ env.WHEELS_TAG }}
|
tag: ${{ matrix.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
21
.gitignore
vendored
@@ -1,10 +1,17 @@
|
|||||||
|
.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]
|
||||||
@@ -14,11 +21,8 @@ hass_frontend/*
|
|||||||
# 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/*
|
||||||
@@ -31,9 +35,8 @@ 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
|
||||||
|
@@ -52,6 +52,7 @@ 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",
|
||||||
@@ -79,13 +80,6 @@ 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"),
|
|
||||||
require.resolve("vis-network/peer/esm/vis-network.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");
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
// 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");
|
||||||
@@ -18,6 +19,11 @@ 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,
|
||||||
@@ -79,7 +85,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/ },
|
{ ignored: /build-translations/, poll: isWsl },
|
||||||
doneHandler()
|
doneHandler()
|
||||||
);
|
);
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
@@ -137,7 +143,7 @@ gulp.task("webpack-watch-hassio", () => {
|
|||||||
isProdBuild: false,
|
isProdBuild: false,
|
||||||
latestBuild: true,
|
latestBuild: true,
|
||||||
})
|
})
|
||||||
).watch({ ignored: /build-translations/ }, doneHandler());
|
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
|
||||||
|
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
path.join(paths.translations_src, "en.json"),
|
path.join(paths.translations_src, "en.json"),
|
||||||
|
@@ -57,7 +57,6 @@ const createRollupConfig = ({
|
|||||||
babel({
|
babel({
|
||||||
...bundle.babelOptions({ latestBuild }),
|
...bundle.babelOptions({ latestBuild }),
|
||||||
extensions,
|
extensions,
|
||||||
exclude: bundle.babelExclude(),
|
|
||||||
babelHelpers: isWDS ? "inline" : "bundled",
|
babelHelpers: isWDS ? "inline" : "bundled",
|
||||||
}),
|
}),
|
||||||
string({
|
string({
|
||||||
|
@@ -47,7 +47,6 @@ 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 }),
|
||||||
@@ -116,8 +115,9 @@ 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(
|
||||||
require.resolve(
|
path.resolve(
|
||||||
"@lit-labs/virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
paths.polymer_dir,
|
||||||
|
"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")
|
||||||
|
@@ -28,10 +28,11 @@ 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,
|
||||||
});
|
});
|
||||||
@@ -64,6 +65,9 @@ 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",
|
||||||
});
|
});
|
||||||
@@ -136,6 +140,7 @@ 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] },
|
||||||
|
@@ -977,6 +977,7 @@ 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}`,
|
||||||
|
@@ -29,7 +29,6 @@ class SupervisorFormfieldLabel extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
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-checkbox";
|
||||||
import "../../../src/components/ha-formfield";
|
import "../../../src/components/ha-formfield";
|
||||||
import "../../../src/components/ha-radio";
|
import "../../../src/components/ha-radio";
|
||||||
@@ -44,6 +45,9 @@ const _computeFolders = (folders): CheckboxItem[] => {
|
|||||||
if (folders.includes("share")) {
|
if (folders.includes("share")) {
|
||||||
list.push({ slug: "share", name: "Share", checked: false });
|
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")) {
|
if (folders.includes("addons/local")) {
|
||||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
||||||
}
|
}
|
||||||
@@ -64,6 +68,8 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
|||||||
export class SupervisorSnapshotContent extends LitElement {
|
export class SupervisorSnapshotContent extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public localize?: LocalizeFunc;
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
|
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
|
||||||
@@ -78,10 +84,14 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public snapshotHasPassword = false;
|
@property({ type: Boolean }) public snapshotHasPassword = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public onboarding = false;
|
||||||
|
|
||||||
@property() public snapshotName = "";
|
@property() public snapshotName = "";
|
||||||
|
|
||||||
@property() public snapshotPassword = "";
|
@property() public snapshotPassword = "";
|
||||||
|
|
||||||
|
@property() public confirmSnapshotPassword = "";
|
||||||
|
|
||||||
public willUpdate(changedProps) {
|
public willUpdate(changedProps) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
@@ -101,8 +111,12 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _localize = (string: string) =>
|
||||||
|
this.supervisor?.localize(`snapshot.${string}`) ||
|
||||||
|
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.supervisor) {
|
if (!this.onboarding && !this.supervisor) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
const foldersSection =
|
const foldersSection =
|
||||||
@@ -114,14 +128,16 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
${this.snapshot
|
${this.snapshot
|
||||||
? html`<div class="details">
|
? html`<div class="details">
|
||||||
${this.snapshot.type === "full"
|
${this.snapshot.type === "full"
|
||||||
? this.supervisor.localize("snapshot.full_snapshot")
|
? this._localize("full_snapshot")
|
||||||
: this.supervisor.localize("snapshot.partial_snapshot")}
|
: this._localize("partial_snapshot")}
|
||||||
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
||||||
${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
|
${this.hass
|
||||||
|
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
|
||||||
|
: this.snapshot.date}
|
||||||
</div>`
|
</div>`
|
||||||
: html`<paper-input
|
: html`<paper-input
|
||||||
name="snapshotName"
|
name="snapshotName"
|
||||||
.label=${this.supervisor.localize("snapshot.name")}
|
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
|
||||||
.value=${this.snapshotName}
|
.value=${this.snapshotName}
|
||||||
@value-changed=${this._handleTextValueChanged}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
@@ -129,13 +145,11 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
${!this.snapshot || this.snapshot.type === "full"
|
${!this.snapshot || this.snapshot.type === "full"
|
||||||
? html`<div class="sub-header">
|
? html`<div class="sub-header">
|
||||||
${!this.snapshot
|
${!this.snapshot
|
||||||
? this.supervisor.localize("snapshot.type")
|
? this._localize("type")
|
||||||
: this.supervisor.localize("snapshot.select_type")}
|
: this._localize("select_type")}
|
||||||
</div>
|
</div>
|
||||||
<div class="snapshot-types">
|
<div class="snapshot-types">
|
||||||
<ha-formfield
|
<ha-formfield .label=${this._localize("full_snapshot")}>
|
||||||
.label=${this.supervisor.localize("snapshot.full_snapshot")}
|
|
||||||
>
|
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="full"
|
value="full"
|
||||||
@@ -144,9 +158,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-radio>
|
</ha-radio>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<ha-formfield
|
<ha-formfield .label=${this._localize("partial_snapshot")}>
|
||||||
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
|
|
||||||
>
|
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="partial"
|
value="partial"
|
||||||
@@ -157,9 +169,9 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
</div>`
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.snapshot && this.snapshotType === "partial"
|
${this.snapshotType === "partial"
|
||||||
? html`
|
? html`<div class="partial-picker">
|
||||||
${this.snapshot.homeassistant
|
${this.snapshot && this.snapshot.homeassistant
|
||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
@@ -179,15 +191,11 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.snapshotType === "partial"
|
|
||||||
? html`
|
|
||||||
${foldersSection?.templates.length
|
${foldersSection?.templates.length
|
||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this.supervisor.localize("snapshot.folders")}
|
.label=${this._localize("folders")}
|
||||||
.iconPath=${mdiFolder}
|
.iconPath=${mdiFolder}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@@ -207,7 +215,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this.supervisor.localize("snapshot.addons")}
|
.label=${this._localize("addons")}
|
||||||
.iconPath=${mdiPuzzle}
|
.iconPath=${mdiPuzzle}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@@ -223,29 +231,44 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
<div class="section-content">${addonsSection.templates}</div>
|
<div class="section-content">${addonsSection.templates}</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
</div> `
|
||||||
|
: ""}
|
||||||
|
${this.snapshotType === "partial" &&
|
||||||
|
(!this.snapshot || this.snapshotHasPassword)
|
||||||
|
? html`<hr />`
|
||||||
: ""}
|
: ""}
|
||||||
${!this.snapshot
|
${!this.snapshot
|
||||||
? html`<ha-formfield
|
? html`<ha-formfield
|
||||||
.label=${this.supervisor.localize("snapshot.password_protection")}
|
class="password"
|
||||||
|
.label=${this._localize("password_protection")}
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
.checked=${this.snapshotHasPassword}
|
.checked=${this.snapshotHasPassword}
|
||||||
@change=${this._toggleHasPassword}
|
@change=${this._toggleHasPassword}
|
||||||
>
|
>
|
||||||
</ha-checkbox
|
</ha-checkbox>
|
||||||
></ha-formfield>`
|
</ha-formfield>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.snapshotHasPassword
|
${this.snapshotHasPassword
|
||||||
? html`
|
? html`
|
||||||
<paper-input
|
<paper-input
|
||||||
.label=${this.supervisor.localize("snapshot.password")}
|
.label=${this._localize("password")}
|
||||||
type="password"
|
type="password"
|
||||||
name="snapshotPassword"
|
name="snapshotPassword"
|
||||||
.value=${this.snapshotPassword}
|
.value=${this.snapshotPassword}
|
||||||
@value-changed=${this._handleTextValueChanged}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</paper-input>
|
</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>`
|
||||||
|
: ""}
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`;
|
`;
|
||||||
@@ -253,21 +276,24 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
ha-checkbox {
|
.partial-picker ha-formfield {
|
||||||
--mdc-checkbox-touch-target-size: 16px;
|
|
||||||
display: block;
|
display: block;
|
||||||
margin: 4px 12px 8px 0;
|
|
||||||
}
|
}
|
||||||
ha-formfield {
|
.partial-picker ha-checkbox {
|
||||||
display: contents;
|
--mdc-checkbox-touch-target-size: 32px;
|
||||||
|
}
|
||||||
|
.partial-picker {
|
||||||
|
display: block;
|
||||||
|
margin: 0px -6px;
|
||||||
}
|
}
|
||||||
supervisor-formfield-label {
|
supervisor-formfield-label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
paper-input[type="password"] {
|
hr {
|
||||||
display: block;
|
border-color: var(--divider-color);
|
||||||
margin: 4px 0 4px 16px;
|
border-bottom: none;
|
||||||
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
.details {
|
.details {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
@@ -275,13 +301,15 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
.section-content {
|
.section-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 16px;
|
margin-left: 30px;
|
||||||
}
|
}
|
||||||
.security {
|
ha-formfield.password {
|
||||||
margin-top: 16px;
|
display: block;
|
||||||
|
margin: 0 -14px -16px;
|
||||||
}
|
}
|
||||||
.snapshot-types {
|
.snapshot-types {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-left: -13px;
|
||||||
}
|
}
|
||||||
.sub-header {
|
.sub-header {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -300,6 +328,9 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
|
|
||||||
if (this.snapshotHasPassword) {
|
if (this.snapshotHasPassword) {
|
||||||
data.password = this.snapshotPassword;
|
data.password = this.snapshotPassword;
|
||||||
|
if (!this.snapshot) {
|
||||||
|
data.confirm_password = this.confirmSnapshotPassword;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.snapshotType === "full") {
|
if (this.snapshotType === "full") {
|
||||||
@@ -331,7 +362,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
const addons =
|
const addons =
|
||||||
section === "addons"
|
section === "addons"
|
||||||
? new Map(
|
? new Map(
|
||||||
this.supervisor!.addon.addons.map((item) => [item.slug, item])
|
this.supervisor?.addon.addons.map((item) => [item.slug, item])
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
let checkedItems = 0;
|
let checkedItems = 0;
|
||||||
@@ -341,6 +372,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
.label=${item.name}
|
.label=${item.name}
|
||||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
||||||
.imageUrl=${section === "addons" &&
|
.imageUrl=${section === "addons" &&
|
||||||
|
!this.onboarding &&
|
||||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||||
addons?.get(item.slug)?.icon
|
addons?.get(item.slug)?.icon
|
||||||
? `/api/hassio/addons/${item.slug}/icon`
|
? `/api/hassio/addons/${item.slug}/icon`
|
||||||
|
@@ -161,6 +161,7 @@ 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}`,
|
||||||
|
194
hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
Executable file
194
hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
Executable file
@@ -0,0 +1,194 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
19
hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts
Normal file
19
hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
@@ -95,16 +95,25 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
this._creatingSnapshot = true;
|
this._creatingSnapshot = true;
|
||||||
|
|
||||||
this._error = "";
|
this._error = "";
|
||||||
if (
|
if (snapshotDetails.password && !snapshotDetails.password.length) {
|
||||||
this._snapshotContent.snapshotHasPassword &&
|
|
||||||
!this._snapshotContent.snapshotPassword.length
|
|
||||||
) {
|
|
||||||
this._error = this._dialogParams!.supervisor.localize(
|
this._error = this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.enter_password"
|
"snapshot.enter_password"
|
||||||
);
|
);
|
||||||
this._creatingSnapshot = false;
|
this._creatingSnapshot = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
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._snapshotContent.snapshotType === "full") {
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
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 { mdiDotsVertical } from "@mdi/js";
|
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||||
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, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { slugify } from "../../../../src/common/string/slugify";
|
import { slugify } from "../../../../src/common/string/slugify";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../../src/components/ha-button-menu";
|
import "../../../../src/components/ha-button-menu";
|
||||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
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";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||||
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 "../../components/supervisor-snapshot-content";
|
||||||
import type { SupervisorSnapshotContent } from "../../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";
|
||||||
@@ -66,14 +67,24 @@ class HassioSnapshotDialog
|
|||||||
open
|
open
|
||||||
scrimClickAction
|
scrimClickAction
|
||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
.heading=${createCloseHeading(this.hass, this._computeName)}
|
.heading=${true}
|
||||||
>
|
>
|
||||||
|
<div slot="heading">
|
||||||
|
<ha-header-bar>
|
||||||
|
<span slot="title">${this._snapshot.name}</span>
|
||||||
|
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</ha-header-bar>
|
||||||
|
</div>
|
||||||
${this._restoringSnapshot
|
${this._restoringSnapshot
|
||||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||||
: html`<supervisor-snapshot-content
|
: html`<supervisor-snapshot-content
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.supervisor=${this._dialogParams.supervisor}
|
.supervisor=${this._dialogParams.supervisor}
|
||||||
.snapshot=${this._snapshot}
|
.snapshot=${this._snapshot}
|
||||||
|
.onboarding=${this._dialogParams.onboarding || false}
|
||||||
|
.localize=${this._dialogParams.localize}
|
||||||
>
|
>
|
||||||
</supervisor-snapshot-content>`}
|
</supervisor-snapshot-content>`}
|
||||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
||||||
@@ -86,18 +97,20 @@ class HassioSnapshotDialog
|
|||||||
Restore
|
Restore
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
|
|
||||||
<ha-button-menu
|
${!this._dialogParams.onboarding
|
||||||
fixed
|
? html`<ha-button-menu
|
||||||
slot="primaryAction"
|
fixed
|
||||||
@action=${this._handleMenuAction}
|
slot="primaryAction"
|
||||||
@closed=${(ev: Event) => ev.stopPropagation()}
|
@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 slot="trigger" alt="menu">
|
||||||
</mwc-icon-button>
|
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
</mwc-icon-button>
|
||||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
<mwc-list-item>Download Snapshot</mwc-list-item>
|
||||||
</ha-button-menu>
|
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
||||||
|
</ha-button-menu>`
|
||||||
|
: ""}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -114,6 +127,12 @@ class HassioSnapshotDialog
|
|||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-on-primary: var(--primary-text-color);
|
||||||
|
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -288,12 +307,11 @@ class HassioSnapshotDialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = document.createElement("a");
|
fileDownload(
|
||||||
a.href = signedPath.path;
|
this,
|
||||||
a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
|
signedPath.path,
|
||||||
this.shadowRoot!.appendChild(a);
|
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
|
||||||
a.click();
|
);
|
||||||
this.shadowRoot!.removeChild(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeName() {
|
private get _computeName() {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -6,6 +7,7 @@ export interface HassioSnapshotDialogParams {
|
|||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
|
localize?: LocalizeFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioSnapshotDialog = (
|
export const showHassioSnapshotDialog = (
|
||||||
|
@@ -2,19 +2,32 @@ 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 {
|
||||||
@@ -22,12 +35,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;
|
||||||
|
|
||||||
@@ -36,14 +49,17 @@ 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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +72,7 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._dialogParams) {
|
if (!this._dialogParams || !this._frontendPrefrences) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@@ -82,6 +98,16 @@ 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"
|
||||||
@@ -94,12 +120,6 @@ 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")}
|
||||||
@@ -133,12 +153,27 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleSnapshot() {
|
private async _toggleSnapshot(): Promise<void> {
|
||||||
this._createSnapshot = !this._createSnapshot;
|
this._frontendPrefrences!.snapshot_before_update[
|
||||||
|
this._dialogParams!.slug
|
||||||
|
] = !snapshot_before_update(
|
||||||
|
this._dialogParams!.slug,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
);
|
||||||
|
|
||||||
|
await saveSupervisorFrontendPreferences(
|
||||||
|
this.hass,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _update() {
|
private async _update() {
|
||||||
if (this._createSnapshot) {
|
if (
|
||||||
|
snapshot_before_update(
|
||||||
|
this._dialogParams!.slug,
|
||||||
|
this._frontendPrefrences!
|
||||||
|
)
|
||||||
|
) {
|
||||||
this._action = "snapshot";
|
this._action = "snapshot";
|
||||||
try {
|
try {
|
||||||
await createHassioPartialSnapshot(
|
await createHassioPartialSnapshot(
|
||||||
|
@@ -5,6 +5,7 @@ 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>;
|
||||||
}
|
}
|
||||||
|
@@ -97,16 +97,23 @@ class HassioIngressView extends LitElement {
|
|||||||
title: requestedAddon,
|
title: requestedAddon,
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
history.back();
|
navigate("/hassio/store", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!addonInfo.ingress) {
|
if (!addonInfo.version) {
|
||||||
|
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();
|
await nextRender();
|
||||||
history.back();
|
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||||
} else {
|
} else {
|
||||||
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
||||||
}
|
}
|
||||||
|
@@ -164,6 +164,7 @@ 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}`,
|
||||||
|
@@ -2,7 +2,6 @@ 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 { dump } 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";
|
||||||
@@ -41,8 +40,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")
|
||||||
@@ -229,20 +228,19 @@ class HassioHostInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _showHardware(): Promise<void> {
|
private async _showHardware(): Promise<void> {
|
||||||
|
let hardware;
|
||||||
try {
|
try {
|
||||||
const content = await fetchHassioHardwareInfo(this.hass);
|
hardware = await fetchHassioHardwareInfo(this.hass);
|
||||||
showHassioMarkdownDialog(this, {
|
|
||||||
title: this.supervisor.localize("system.host.hardware"),
|
|
||||||
content: `<pre>${dump(content, { indent: 2 })}</pre>`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
await 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> {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
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",
|
||||||
};
|
};
|
||||||
|
@@ -66,9 +66,7 @@
|
|||||||
"@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-checkbox": "^3.1.0",
|
"@polymer/paper-checkbox": "^3.1.0",
|
||||||
@@ -108,7 +106,7 @@
|
|||||||
"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.4",
|
"hls.js": "^1.0.5",
|
||||||
"home-assistant-js-websocket": "^5.10.0",
|
"home-assistant-js-websocket": "^5.10.0",
|
||||||
"idb-keyval": "^5.0.5",
|
"idb-keyval": "^5.0.5",
|
||||||
"intl-messageformat": "^9.6.16",
|
"intl-messageformat": "^9.6.16",
|
||||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20210531.0",
|
version="20210603.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",
|
||||||
|
@@ -6,8 +6,7 @@ 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");
|
||||||
@@ -17,10 +16,6 @@ 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");
|
||||||
|
@@ -89,8 +89,6 @@ export const domainIcon = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.warn(
|
console.warn(`Unable to find icon for domain ${domain}`);
|
||||||
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
|
|
||||||
);
|
|
||||||
return DEFAULT_DOMAIN_ICON;
|
return DEFAULT_DOMAIN_ICON;
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import { customElement, property } from "lit/decorators";
|
import {
|
||||||
import { classMap } from "lit/directives/class-map";
|
css,
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -27,18 +34,11 @@ 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,6 +62,17 @@ 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) });
|
||||||
}
|
}
|
||||||
|
@@ -4,29 +4,25 @@
|
|||||||
// 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 (...args) => unknown>(
|
export const debounce = <T extends any[]>(
|
||||||
func: T,
|
func: (...args: T) => void,
|
||||||
wait,
|
wait: number,
|
||||||
immediate = false
|
immediate = false
|
||||||
): T => {
|
) => {
|
||||||
let timeout;
|
let timeout: number | undefined;
|
||||||
// @ts-ignore
|
return (...args: T): void => {
|
||||||
return function (...args) {
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const context = this;
|
|
||||||
const later = () => {
|
const later = () => {
|
||||||
timeout = null;
|
timeout = undefined;
|
||||||
if (!immediate) {
|
if (!immediate) {
|
||||||
func.apply(context, args);
|
func(...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const callNow = immediate && !timeout;
|
const callNow = immediate && !timeout;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = window.setTimeout(later, wait);
|
||||||
if (callNow) {
|
if (callNow) {
|
||||||
func.apply(context, args);
|
func(...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -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 + 57}px`
|
? `${(this._filteredData.length || 1) * 53 + 53}px`
|
||||||
: `calc(100% - ${this._headerHeight}px)`,
|
: `calc(100% - ${this._headerHeight}px)`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -340,11 +340,10 @@ 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...
|
// not sure how this happens...
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return "";
|
return html``;
|
||||||
}
|
}
|
||||||
if (row.append) {
|
if (row.append) {
|
||||||
return html`
|
return html`
|
||||||
@@ -920,13 +919,11 @@ 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 {
|
|
||||||
overflow: auto;
|
.mdc-data-table__table.auto-height .scroller {
|
||||||
|
overflow-y: hidden !important;
|
||||||
}
|
}
|
||||||
.grows {
|
.grows {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@@ -14,12 +14,17 @@ 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 name="header">${this.header}</slot>
|
<slot class="header" name="header">
|
||||||
|
${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 })}"
|
||||||
@@ -106,6 +111,16 @@ 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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,14 +7,14 @@ import { afterNextRender } from "../common/util/render-status";
|
|||||||
import { FrontendLocaleData } from "../data/translation";
|
import { FrontendLocaleData } 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;
|
||||||
|
@@ -13,6 +13,11 @@ 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;
|
||||||
@@ -39,7 +44,7 @@ class HaHLSPlayer extends LitElement {
|
|||||||
|
|
||||||
@state() private _attached = false;
|
@state() private _attached = false;
|
||||||
|
|
||||||
private _hlsPolyfillInstance?: HlsType;
|
private _hlsPolyfillInstance?: HlsLite;
|
||||||
|
|
||||||
private _useExoPlayer = false;
|
private _useExoPlayer = false;
|
||||||
|
|
||||||
@@ -103,7 +108,8 @@ class HaHLSPlayer extends LitElement {
|
|||||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||||
const masterPlaylistPromise = fetch(this.url);
|
const masterPlaylistPromise = fetch(this.url);
|
||||||
|
|
||||||
const Hls = (await import("hls.js")).default;
|
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js"))
|
||||||
|
.default;
|
||||||
let hlsSupported = Hls.isSupported();
|
let hlsSupported = Hls.isSupported();
|
||||||
|
|
||||||
if (!hlsSupported) {
|
if (!hlsSupported) {
|
||||||
@@ -182,7 +188,7 @@ class HaHLSPlayer extends LitElement {
|
|||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
const hls = new Hls({
|
const hls = new Hls({
|
||||||
liveBackBufferLength: 60,
|
backBufferLength: 60,
|
||||||
fragLoadingTimeOut: 30000,
|
fragLoadingTimeOut: 30000,
|
||||||
manifestLoadingTimeOut: 30000,
|
manifestLoadingTimeOut: 30000,
|
||||||
levelLoadingTimeOut: 30000,
|
levelLoadingTimeOut: 30000,
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
import { customElement, state, property } from "lit/decorators";
|
import { customElement, state, property } from "lit/decorators";
|
||||||
import {
|
import {
|
||||||
Adapter,
|
Adapter,
|
||||||
@@ -17,18 +24,19 @@ import "./ha-icon";
|
|||||||
|
|
||||||
const format_addresses = (
|
const format_addresses = (
|
||||||
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
|
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
|
||||||
): TemplateResult[] =>
|
): TemplateResult =>
|
||||||
addresses.map(
|
html`${addresses.map((address, i) => [
|
||||||
(address) => html`<span>${address.address}/${address.network_prefix}</span>`
|
html`<span>${address.address}/${address.network_prefix}</span>`,
|
||||||
);
|
i < addresses.length - 1 ? ", " : nothing,
|
||||||
|
])}`;
|
||||||
|
|
||||||
const format_auto_detected_interfaces = (
|
const format_auto_detected_interfaces = (
|
||||||
adapters: Adapter[]
|
adapters: Adapter[]
|
||||||
): Array<TemplateResult | string> =>
|
): Array<TemplateResult | string> =>
|
||||||
adapters.map((adapter) =>
|
adapters.map((adapter) =>
|
||||||
adapter.auto
|
adapter.auto
|
||||||
? html`${adapter.name} (${format_addresses(adapter.ipv4)}
|
? html`${adapter.name}
|
||||||
${format_addresses(adapter.ipv6)} )`
|
(${format_addresses([...adapter.ipv4, ...adapter.ipv6])})`
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,8 +96,7 @@ export class HaNetwork extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
<span slot="description">
|
<span slot="description">
|
||||||
${format_addresses(adapter.ipv4)}
|
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||||
${format_addresses(adapter.ipv6)}
|
|
||||||
</span>
|
</span>
|
||||||
</ha-settings-row>`
|
</ha-settings-row>`
|
||||||
)
|
)
|
||||||
|
69
src/components/map/ha-entity-marker.ts
Normal file
69
src/components/map/ha-entity-marker.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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);
|
@@ -1,299 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,10 +3,8 @@ import {
|
|||||||
DivIcon,
|
DivIcon,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
LatLng,
|
LatLng,
|
||||||
Map,
|
|
||||||
Marker,
|
Marker,
|
||||||
MarkerOptions,
|
MarkerOptions,
|
||||||
TileLayer,
|
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -16,15 +14,13 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import {
|
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||||
LeafletModuleType,
|
import type { HomeAssistant } from "../../types";
|
||||||
replaceTileLayer,
|
import "./ha-map";
|
||||||
setupLeafletMap,
|
import type { HaMap } from "./ha-map";
|
||||||
} 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
|
||||||
@@ -51,38 +47,40 @@ 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() public locations?: MarkerLocation[];
|
@property({ attribute: false }) public locations?: MarkerLocation[];
|
||||||
|
|
||||||
public fitZoom = 16;
|
@property({ type: Boolean }) public autoFit = false;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
constructor() {
|
||||||
private _leafletMap?: Map;
|
super();
|
||||||
|
|
||||||
private _tileLayer?: TileLayer;
|
import("leaflet").then((module) => {
|
||||||
|
import("leaflet-draw").then(() => {
|
||||||
private _locationMarkers?: { [key: string]: Marker | Circle };
|
this.Leaflet = module.default as LeafletModuleType;
|
||||||
|
this._updateMarkers();
|
||||||
private _circles: Record<string, Circle> = {};
|
this.updateComplete.then(() => this.fitMap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(): void {
|
||||||
if (
|
this.map.fitMap();
|
||||||
!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._leafletMap || !this._locationMarkers) {
|
if (!this.map.leafletMap || !this._locationMarkers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const marker = this._locationMarkers[id];
|
const marker = this._locationMarkers[id];
|
||||||
@@ -90,29 +88,44 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ("getBounds" in marker) {
|
if ("getBounds" in marker) {
|
||||||
this._leafletMap.fitBounds(marker.getBounds());
|
this.map.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._leafletMap.fitBounds(circle.getBounds());
|
this.map.leafletMap.fitBounds(circle.getBounds());
|
||||||
} else {
|
} else {
|
||||||
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
|
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html` <div id="map"></div> `;
|
return html`<ha-map
|
||||||
|
.hass=${this.hass}
|
||||||
|
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||||
|
.zoom=${this.zoom}
|
||||||
|
.autoFit=${this.autoFit}
|
||||||
|
.darkMode=${this.darkMode}
|
||||||
|
></ha-map>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
private _getLayers = memoizeOne(
|
||||||
super.firstUpdated(changedProps);
|
(
|
||||||
this._initMap();
|
circles: Record<string, Circle>,
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
// Still loading.
|
// Still loading.
|
||||||
if (!this.Leaflet) {
|
if (!this.Leaflet) {
|
||||||
@@ -122,37 +135,6 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
if (changedProps.has("locations")) {
|
if (changedProps.has("locations")) {
|
||||||
this._updateMarkers();
|
this._updateMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("hass")) {
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this._leafletMap || !this._tileLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._tileLayer = replaceTileLayer(
|
|
||||||
this.Leaflet,
|
|
||||||
this._leafletMap,
|
|
||||||
this._tileLayer,
|
|
||||||
this.hass.themes.darkMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _mapEl(): HTMLDivElement {
|
|
||||||
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) {
|
||||||
@@ -189,21 +171,18 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateMarkers(): void {
|
private _updateMarkers(): void {
|
||||||
if (this._locationMarkers) {
|
|
||||||
Object.values(this._locationMarkers).forEach((marker) => {
|
|
||||||
marker.remove();
|
|
||||||
});
|
|
||||||
this._locationMarkers = undefined;
|
|
||||||
|
|
||||||
Object.values(this._circles).forEach((circle) => circle.remove());
|
|
||||||
this._circles = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.locations || !this.locations.length) {
|
if (!this.locations || !this.locations.length) {
|
||||||
|
this._circles = {};
|
||||||
|
this._locationMarkers = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._locationMarkers = {};
|
const 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;
|
||||||
@@ -228,45 +207,46 @@ 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 || defaultRadiusColor,
|
color: location.radius_color || defaultZoneRadiusColor,
|
||||||
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();
|
||||||
// @ts-ignore
|
circle.addEventListener("add", () => {
|
||||||
const moveMarker = circle.editing._moveMarker;
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const moveMarker = circle.editing._moveMarker;
|
||||||
const resizeMarker = circle.editing._resizeMarkers[0];
|
// @ts-ignore
|
||||||
if (icon) {
|
const resizeMarker = circle.editing._resizeMarkers[0];
|
||||||
moveMarker.setIcon(icon);
|
if (icon) {
|
||||||
}
|
moveMarker.setIcon(icon);
|
||||||
resizeMarker.id = moveMarker.id = location.id;
|
}
|
||||||
moveMarker
|
resizeMarker.id = moveMarker.id = location.id;
|
||||||
.addEventListener(
|
moveMarker
|
||||||
"dragend",
|
.addEventListener(
|
||||||
// @ts-ignore
|
"dragend",
|
||||||
(ev: DragEndEvent) => this._updateLocation(ev)
|
// @ts-ignore
|
||||||
)
|
(ev: DragEndEvent) => this._updateLocation(ev)
|
||||||
.addEventListener(
|
)
|
||||||
"click",
|
.addEventListener(
|
||||||
// @ts-ignore
|
"click",
|
||||||
(ev: MouseEvent) => this._markerClicked(ev)
|
// @ts-ignore
|
||||||
);
|
(ev: MouseEvent) => this._markerClicked(ev)
|
||||||
if (location.radius_editable) {
|
);
|
||||||
resizeMarker.addEventListener(
|
if (location.radius_editable) {
|
||||||
"dragend",
|
resizeMarker.addEventListener(
|
||||||
// @ts-ignore
|
"dragend",
|
||||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
// @ts-ignore
|
||||||
);
|
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||||
} else {
|
);
|
||||||
resizeMarker.remove();
|
} else {
|
||||||
}
|
resizeMarker.remove();
|
||||||
this._locationMarkers![location.id] = circle;
|
}
|
||||||
|
});
|
||||||
|
locationMarkers[location.id] = circle;
|
||||||
} else {
|
} else {
|
||||||
this._circles[location.id] = circle;
|
circles[location.id] = circle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -275,6 +255,7 @@ 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) {
|
||||||
@@ -293,13 +274,14 @@ 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;
|
||||||
|
|
||||||
this._locationMarkers![location.id] = marker;
|
locationMarkers[location.id] = marker;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this._circles = circles;
|
||||||
|
this._locationMarkers = locationMarkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -308,23 +290,9 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
#map {
|
ha-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;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
|
|
||||||
import {
|
import {
|
||||||
css,
|
Circle,
|
||||||
CSSResultGroup,
|
CircleMarker,
|
||||||
html,
|
LatLngTuple,
|
||||||
LitElement,
|
Layer,
|
||||||
PropertyValues,
|
Map,
|
||||||
TemplateResult,
|
Marker,
|
||||||
} from "lit";
|
Polyline,
|
||||||
import { customElement, property } from "lit/decorators";
|
TileLayer,
|
||||||
|
} from "leaflet";
|
||||||
|
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import {
|
import {
|
||||||
LeafletModuleType,
|
LeafletModuleType,
|
||||||
replaceTileLayer,
|
replaceTileLayer,
|
||||||
@@ -15,194 +17,324 @@ 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 { debounce } from "../../common/util/debounce";
|
import "./ha-entity-marker";
|
||||||
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")
|
||||||
class HaMap extends LitElement {
|
export class HaMap extends ReactiveElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public entities?: string[];
|
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
|
||||||
|
|
||||||
@property() public darkMode?: boolean;
|
@property({ attribute: false }) public paths?: HaMapPaths[];
|
||||||
|
|
||||||
@property() public zoom?: number;
|
@property({ attribute: false }) public layers?: Layer[];
|
||||||
|
|
||||||
|
@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 _connected = false;
|
private _mapPaths: Array<Polyline | CircleMarker> = [];
|
||||||
|
|
||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._connected = true;
|
this._loadMap();
|
||||||
if (this.hasUpdated) {
|
this._attachObserver();
|
||||||
this.loadMap();
|
|
||||||
this._attachObserver();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnectedCallback(): void {
|
public disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._connected = false;
|
if (this.leafletMap) {
|
||||||
|
this.leafletMap.remove();
|
||||||
if (this._leafletMap) {
|
this.leafletMap = undefined;
|
||||||
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._mapEl);
|
this._resizeObserver.unobserve(this);
|
||||||
} else {
|
|
||||||
window.removeEventListener("resize", this._debouncedResizeListener);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected update(changedProps: PropertyValues) {
|
||||||
if (!this.entities) {
|
super.update(changedProps);
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
return html` <div id="map"></div> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
if (!this._loaded) {
|
||||||
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;
|
|
||||||
|
|
||||||
if (!oldHass || !this.entities) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any state has changed
|
|
||||||
for (const entity of this.entities) {
|
|
||||||
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
|
||||||
if (changedProps.has("hass")) {
|
|
||||||
this._drawEntities();
|
|
||||||
this._fitMap();
|
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._tileLayer = replaceTileLayer(
|
|
||||||
this.Leaflet,
|
|
||||||
this._leafletMap,
|
|
||||||
this._tileLayer,
|
|
||||||
this.hass.themes.darkMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _mapEl(): HTMLDivElement {
|
|
||||||
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadMap(): Promise<void> {
|
|
||||||
[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) {
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
this._leafletMap.setView(
|
|
||||||
|
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||||
|
this._drawEntities();
|
||||||
|
} else if (this._loaded && oldHass && this.entities) {
|
||||||
|
// Check if any state has changed
|
||||||
|
for (const entity of this.entities) {
|
||||||
|
if (
|
||||||
|
oldHass.states[getEntityId(entity)] !==
|
||||||
|
this.hass!.states[getEntityId(entity)]
|
||||||
|
) {
|
||||||
|
this._drawEntities();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||||
|
this._drawPaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("_loaded") || changedProps.has("layers")) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||||
|
this._tileLayer = replaceTileLayer(
|
||||||
|
this.Leaflet!,
|
||||||
|
this.leafletMap!,
|
||||||
|
this._tileLayer!,
|
||||||
|
darkMode
|
||||||
|
);
|
||||||
|
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadMap(): Promise<void> {
|
||||||
|
let map = this.shadowRoot!.getElementById("map");
|
||||||
|
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 {
|
||||||
|
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._mapItems.length && !this.layers?.length) {
|
||||||
|
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 || 14
|
this.zoom
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bounds = this.Leaflet.latLngBounds(
|
let 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.zoom && this._leafletMap.getZoom() > this.zoom) {
|
if (this.fitZones) {
|
||||||
this._leafletMap.setZoom(this.zoom);
|
this._mapZones?.forEach((zone) => {
|
||||||
|
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) {
|
if (this._mapItems.length) {
|
||||||
this._mapItems.forEach((marker) => marker.remove());
|
this._mapItems.forEach((marker) => marker.remove());
|
||||||
|
this._mapItems = [];
|
||||||
}
|
}
|
||||||
const mapItems: Layer[] = (this._mapItems = []);
|
|
||||||
|
|
||||||
if (this._mapZones) {
|
if (this._mapZones.length) {
|
||||||
this._mapZones.forEach((marker) => marker.remove());
|
this._mapZones.forEach((marker) => marker.remove());
|
||||||
|
this._mapZones = [];
|
||||||
}
|
}
|
||||||
const mapZones: Layer[] = (this._mapZones = []);
|
|
||||||
|
|
||||||
const allEntities = this.entities!.concat();
|
if (!this.entities) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const entity of allEntities) {
|
const computedStyles = getComputedStyle(this);
|
||||||
const entityId = entity;
|
const zoneColor = computedStyles.getPropertyValue("--accent-color");
|
||||||
const stateObj = hass.states[entityId];
|
const darkPrimaryColor = computedStyles.getPropertyValue(
|
||||||
|
"--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;
|
||||||
}
|
}
|
||||||
@@ -240,13 +372,12 @@ class HaMap extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create marker with the icon
|
// create marker with the icon
|
||||||
mapZones.push(
|
this._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,
|
||||||
@@ -254,10 +385,10 @@ class HaMap extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// create circle around it
|
// create circle around it
|
||||||
mapZones.push(
|
this._mapZones.push(
|
||||||
Leaflet.circle([latitude, longitude], {
|
Leaflet.circle([latitude, longitude], {
|
||||||
interactive: false,
|
interactive: false,
|
||||||
color: "#FF9800",
|
color: zoneColor,
|
||||||
radius,
|
radius,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -273,17 +404,20 @@ class HaMap extends LitElement {
|
|||||||
.join("")
|
.join("")
|
||||||
.substr(0, 3);
|
.substr(0, 3);
|
||||||
|
|
||||||
// create market with the icon
|
// create marker with the icon
|
||||||
mapItems.push(
|
this._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="${entityId}"
|
entity-id="${getEntityId(entity)}"
|
||||||
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],
|
||||||
@@ -295,10 +429,10 @@ class HaMap extends LitElement {
|
|||||||
|
|
||||||
// create circle around if entity has accuracy
|
// create circle around if entity has accuracy
|
||||||
if (gpsAccuracy) {
|
if (gpsAccuracy) {
|
||||||
mapItems.push(
|
this._mapItems.push(
|
||||||
Leaflet.circle([latitude, longitude], {
|
Leaflet.circle([latitude, longitude], {
|
||||||
interactive: false,
|
interactive: false,
|
||||||
color: "#0288D1",
|
color: darkPrimaryColor,
|
||||||
radius: gpsAccuracy,
|
radius: gpsAccuracy,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -309,20 +443,14 @@ class HaMap extends LitElement {
|
|||||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _attachObserver(): void {
|
private async _attachObserver(): Promise<void> {
|
||||||
// Observe changes to map size and invalidate to prevent broken rendering
|
if (!this._resizeObserver) {
|
||||||
// Uses ResizeObserver in Chrome, otherwise window resize event
|
await installResizeObserver();
|
||||||
|
this._resizeObserver = new ResizeObserver(() => {
|
||||||
// @ts-ignore
|
this.leafletMap?.invalidateSize({ debounceMoveend: true });
|
||||||
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 {
|
||||||
@@ -337,13 +465,25 @@ class HaMap extends LitElement {
|
|||||||
#map.dark {
|
#map.dark {
|
||||||
background: #090909;
|
background: #090909;
|
||||||
}
|
}
|
||||||
|
.light {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
.dark {
|
.dark {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
.leaflet-marker-draggable {
|
||||||
.light {
|
cursor: move !important;
|
||||||
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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -377,7 +377,10 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
|||||||
major: {
|
major: {
|
||||||
fontStyle: "bold",
|
fontStyle: "bold",
|
||||||
},
|
},
|
||||||
|
source: "auto",
|
||||||
|
sampleSize: 5,
|
||||||
autoSkipPadding: 20,
|
autoSkipPadding: 20,
|
||||||
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -236,7 +236,9 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
|||||||
major: {
|
major: {
|
||||||
fontStyle: "bold",
|
fontStyle: "bold",
|
||||||
},
|
},
|
||||||
|
sampleSize: 5,
|
||||||
autoSkipPadding: 50,
|
autoSkipPadding: 50,
|
||||||
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
categoryPercentage: undefined,
|
categoryPercentage: undefined,
|
||||||
barPercentage: undefined,
|
barPercentage: undefined,
|
||||||
|
@@ -12,20 +12,20 @@ export interface ConfigEntry {
|
|||||||
| "setup_retry"
|
| "setup_retry"
|
||||||
| "not_loaded"
|
| "not_loaded"
|
||||||
| "failed_unload";
|
| "failed_unload";
|
||||||
connection_class: string;
|
|
||||||
supports_options: boolean;
|
supports_options: boolean;
|
||||||
supports_unload: boolean;
|
supports_unload: boolean;
|
||||||
|
pref_disable_new_entities: boolean;
|
||||||
|
pref_disable_polling: boolean;
|
||||||
disabled_by: "user" | null;
|
disabled_by: "user" | null;
|
||||||
reason: string | null;
|
reason: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigEntryMutableParams {
|
export type ConfigEntryMutableParams = Partial<
|
||||||
title: string;
|
Pick<
|
||||||
}
|
ConfigEntry,
|
||||||
|
"title" | "pref_disable_new_entities" | "pref_disable_polling"
|
||||||
export interface ConfigEntrySystemOptions {
|
>
|
||||||
disable_new_entities: boolean;
|
>;
|
||||||
}
|
|
||||||
|
|
||||||
export const getConfigEntries = (hass: HomeAssistant) =>
|
export const getConfigEntries = (hass: HomeAssistant) =>
|
||||||
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
||||||
@@ -33,9 +33,9 @@ export const getConfigEntries = (hass: HomeAssistant) =>
|
|||||||
export const updateConfigEntry = (
|
export const updateConfigEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
configEntryId: string,
|
configEntryId: string,
|
||||||
updatedValues: Partial<ConfigEntryMutableParams>
|
updatedValues: ConfigEntryMutableParams
|
||||||
) =>
|
) =>
|
||||||
hass.callWS<ConfigEntry>({
|
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({
|
||||||
type: "config_entries/update",
|
type: "config_entries/update",
|
||||||
entry_id: configEntryId,
|
entry_id: configEntryId,
|
||||||
...updatedValues,
|
...updatedValues,
|
||||||
@@ -51,13 +51,15 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
|||||||
require_restart: boolean;
|
require_restart: boolean;
|
||||||
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
|
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
|
||||||
|
|
||||||
|
export interface DisableConfigEntryResult {
|
||||||
|
require_restart: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const disableConfigEntry = (
|
export const disableConfigEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
configEntryId: string
|
configEntryId: string
|
||||||
) =>
|
) =>
|
||||||
hass.callWS<{
|
hass.callWS<DisableConfigEntryResult>({
|
||||||
require_restart: boolean;
|
|
||||||
}>({
|
|
||||||
type: "config_entries/disable",
|
type: "config_entries/disable",
|
||||||
entry_id: configEntryId,
|
entry_id: configEntryId,
|
||||||
disabled_by: "user",
|
disabled_by: "user",
|
||||||
@@ -71,23 +73,3 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
|||||||
entry_id: configEntryId,
|
entry_id: configEntryId,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getConfigEntrySystemOptions = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
configEntryId: string
|
|
||||||
) =>
|
|
||||||
hass.callWS<ConfigEntrySystemOptions>({
|
|
||||||
type: "config_entries/system_options/list",
|
|
||||||
entry_id: configEntryId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateConfigEntrySystemOptions = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
configEntryId: string,
|
|
||||||
params: Partial<ConfigEntrySystemOptions>
|
|
||||||
) =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "config_entries/system_options/update",
|
|
||||||
entry_id: configEntryId,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
@@ -14,12 +14,17 @@ interface HassioHardwareAudioList {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HardwareDevice {
|
||||||
|
attributes: Record<string, string>;
|
||||||
|
by_id: null | string;
|
||||||
|
dev_path: string;
|
||||||
|
name: string;
|
||||||
|
subsystem: string;
|
||||||
|
sysfs: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HassioHardwareInfo {
|
export interface HassioHardwareInfo {
|
||||||
serial: string[];
|
devices: HardwareDevice[];
|
||||||
input: string[];
|
|
||||||
disk: string[];
|
|
||||||
gpio: string[];
|
|
||||||
audio: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioHardwareAudio = async (
|
export const fetchHassioHardwareAudio = async (
|
||||||
|
@@ -41,6 +41,7 @@ export interface HassioSnapshotDetail extends HassioSnapshot {
|
|||||||
export interface HassioFullSnapshotCreateParams {
|
export interface HassioFullSnapshotCreateParams {
|
||||||
name: string;
|
name: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
confirm_password?: string;
|
||||||
}
|
}
|
||||||
export interface HassioPartialSnapshotCreateParams
|
export interface HassioPartialSnapshotCreateParams
|
||||||
extends HassioFullSnapshotCreateParams {
|
extends HassioFullSnapshotCreateParams {
|
||||||
|
@@ -2,6 +2,7 @@ import { Connection, getCollection } from "home-assistant-js-websocket";
|
|||||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
import { LocalizeFunc } from "../../common/translations/localize";
|
import { LocalizeFunc } from "../../common/translations/localize";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { fetchFrontendUserData, saveFrontendUserData } from "../frontend";
|
||||||
import { HassioAddonsInfo } from "../hassio/addon";
|
import { HassioAddonsInfo } from "../hassio/addon";
|
||||||
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
||||||
import { NetworkInfo } from "../hassio/network";
|
import { NetworkInfo } from "../hassio/network";
|
||||||
@@ -13,6 +14,28 @@ import {
|
|||||||
} from "../hassio/supervisor";
|
} from "../hassio/supervisor";
|
||||||
import { SupervisorStore } from "./store";
|
import { SupervisorStore } from "./store";
|
||||||
|
|
||||||
|
export interface SupervisorFrontendPrefrences {
|
||||||
|
snapshot_before_update: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface FrontendUserData {
|
||||||
|
supervisor: SupervisorFrontendPrefrences;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchSupervisorFrontendPreferences = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<SupervisorFrontendPrefrences> => {
|
||||||
|
const stored = await fetchFrontendUserData(hass.connection, "supervisor");
|
||||||
|
return stored || { snapshot_before_update: {} };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveSupervisorFrontendPreferences = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: SupervisorFrontendPrefrences
|
||||||
|
) => saveFrontendUserData(hass.connection, "supervisor", data);
|
||||||
|
|
||||||
export const supervisorWSbaseCommand = {
|
export const supervisorWSbaseCommand = {
|
||||||
type: "supervisor/api",
|
type: "supervisor/api",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
import {
|
|
||||||
DEFAULT_ACCENT_COLOR,
|
|
||||||
DEFAULT_PRIMARY_COLOR,
|
|
||||||
} from "../resources/ha-style";
|
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
|
|
||||||
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
|
|
||||||
export const passiveRadiusColor = "#9b9b9b";
|
|
||||||
|
|
||||||
export interface Zone {
|
export interface Zone {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@@ -3,17 +3,17 @@ import { css, CSSResultGroup, 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";
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import "../../components/ha-circular-progress";
|
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
import "../../components/ha-formfield";
|
import "../../components/ha-formfield";
|
||||||
import "../../components/ha-switch";
|
import "../../components/ha-switch";
|
||||||
import type { HaSwitch } from "../../components/ha-switch";
|
import type { HaSwitch } from "../../components/ha-switch";
|
||||||
import {
|
import {
|
||||||
getConfigEntrySystemOptions,
|
ConfigEntryMutableParams,
|
||||||
updateConfigEntrySystemOptions,
|
updateConfigEntry,
|
||||||
} from "../../data/config_entries";
|
} from "../../data/config_entries";
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||||
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
||||||
|
|
||||||
@customElement("dialog-config-entry-system-options")
|
@customElement("dialog-config-entry-system-options")
|
||||||
@@ -22,12 +22,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
|
|
||||||
@state() private _disableNewEntities!: boolean;
|
@state() private _disableNewEntities!: boolean;
|
||||||
|
|
||||||
|
@state() private _disablePolling!: boolean;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _params?: ConfigEntrySystemOptionsDialogParams;
|
@state() private _params?: ConfigEntrySystemOptionsDialogParams;
|
||||||
|
|
||||||
@state() private _loading = false;
|
|
||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
@@ -35,13 +35,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._loading = true;
|
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||||
const systemOptions = await getConfigEntrySystemOptions(
|
this._disablePolling = params.entry.pref_disable_polling;
|
||||||
this.hass,
|
|
||||||
params.entry.entry_id
|
|
||||||
);
|
|
||||||
this._loading = false;
|
|
||||||
this._disableNewEntities = systemOptions.disable_new_entities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
@@ -66,45 +61,57 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
this._params.entry.domain
|
this._params.entry.domain
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||||
${this._loading
|
<ha-formfield
|
||||||
? html`
|
.label=${html`<p>
|
||||||
<div class="init-spinner">
|
${this.hass.localize(
|
||||||
<ha-circular-progress active></ha-circular-progress>
|
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
|
||||||
</div>
|
)}
|
||||||
`
|
</p>
|
||||||
: html`
|
<p class="secondary">
|
||||||
${this._error
|
${this.hass.localize(
|
||||||
? html` <div class="error">${this._error}</div> `
|
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
|
||||||
: ""}
|
"integration",
|
||||||
<div class="form">
|
this.hass.localize(
|
||||||
<ha-formfield
|
`component.${this._params.entry.domain}.title`
|
||||||
.label=${html`<p>
|
) || this._params.entry.domain
|
||||||
${this.hass.localize(
|
)}
|
||||||
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
|
</p>`}
|
||||||
)}
|
.dir=${computeRTLDirection(this.hass)}
|
||||||
</p>
|
>
|
||||||
<p class="secondary">
|
<ha-switch
|
||||||
${this.hass.localize(
|
.checked=${!this._disableNewEntities}
|
||||||
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
|
@change=${this._disableNewEntitiesChanged}
|
||||||
"integration",
|
.disabled=${this._submitting}
|
||||||
this.hass.localize(
|
></ha-switch>
|
||||||
`component.${this._params.entry.domain}.title`
|
</ha-formfield>
|
||||||
) || this._params.entry.domain
|
${this._allowUpdatePolling()
|
||||||
)}
|
? html`
|
||||||
</p>`}
|
<ha-formfield
|
||||||
.dir=${computeRTLDirection(this.hass)}
|
.label=${html`<p>
|
||||||
>
|
${this.hass.localize(
|
||||||
<ha-switch
|
"ui.dialogs.config_entry_system_options.enable_polling_label"
|
||||||
.checked=${!this._disableNewEntities}
|
)}
|
||||||
@change=${this._disableNewEntitiesChanged}
|
</p>
|
||||||
.disabled=${this._submitting}
|
<p class="secondary">
|
||||||
>
|
${this.hass.localize(
|
||||||
</ha-switch>
|
"ui.dialogs.config_entry_system_options.enable_polling_description",
|
||||||
</ha-formfield>
|
"integration",
|
||||||
</div>
|
this.hass.localize(
|
||||||
`}
|
`component.${this._params.entry.domain}.title`
|
||||||
</div>
|
) || this._params.entry.domain
|
||||||
|
)}
|
||||||
|
</p>`}
|
||||||
|
.dir=${computeRTLDirection(this.hass)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${!this._disablePolling}
|
||||||
|
@change=${this._disablePollingChanged}
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<mwc-button
|
<mwc-button
|
||||||
slot="secondaryAction"
|
slot="secondaryAction"
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
@@ -115,7 +122,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
<mwc-button
|
<mwc-button
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
@click="${this._updateEntry}"
|
@click="${this._updateEntry}"
|
||||||
.disabled=${this._submitting || this._loading}
|
.disabled=${this._submitting}
|
||||||
>
|
>
|
||||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
@@ -123,22 +130,47 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _allowUpdatePolling() {
|
||||||
|
return (
|
||||||
|
this._params!.manifest &&
|
||||||
|
(this._params!.manifest.iot_class === "local_polling" ||
|
||||||
|
this._params!.manifest.iot_class === "cloud_polling")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _disableNewEntitiesChanged(ev: Event): void {
|
private _disableNewEntitiesChanged(ev: Event): void {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._disableNewEntities = !(ev.target as HaSwitch).checked;
|
this._disableNewEntities = !(ev.target as HaSwitch).checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _disablePollingChanged(ev: Event): void {
|
||||||
|
this._error = undefined;
|
||||||
|
this._disablePolling = !(ev.target as HaSwitch).checked;
|
||||||
|
}
|
||||||
|
|
||||||
private async _updateEntry(): Promise<void> {
|
private async _updateEntry(): Promise<void> {
|
||||||
this._submitting = true;
|
this._submitting = true;
|
||||||
|
const data: ConfigEntryMutableParams = {
|
||||||
|
pref_disable_new_entities: this._disableNewEntities,
|
||||||
|
};
|
||||||
|
if (this._allowUpdatePolling()) {
|
||||||
|
data.pref_disable_polling = this._disablePolling;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await updateConfigEntrySystemOptions(
|
const result = await updateConfigEntry(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._params!.entry.entry_id,
|
this._params!.entry.entry_id,
|
||||||
{
|
data
|
||||||
disable_new_entities: this._disableNewEntities,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
this._params = undefined;
|
if (result.require_restart) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.dialogs.config_entry_system_options.restart_home_assistant"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._params!.entryUpdated(result.config_entry);
|
||||||
|
this.closeDialog();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message || "Unknown error";
|
this._error = err.message || "Unknown error";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -150,20 +182,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
.init-spinner {
|
|
||||||
padding: 50px 100px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
padding-top: 6px;
|
|
||||||
padding-bottom: 24px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.secondary {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { ConfigEntry } from "../../data/config_entries";
|
import { ConfigEntry } from "../../data/config_entries";
|
||||||
|
import { IntegrationManifest } from "../../data/integration";
|
||||||
|
|
||||||
export interface ConfigEntrySystemOptionsDialogParams {
|
export interface ConfigEntrySystemOptionsDialogParams {
|
||||||
entry: ConfigEntry;
|
entry: ConfigEntry;
|
||||||
// updateEntry: (
|
manifest?: IntegrationManifest;
|
||||||
// updates: Partial<EntityRegistryEntryUpdateParams>
|
entryUpdated(entry: ConfigEntry): void;
|
||||||
// ) => Promise<unknown>;
|
|
||||||
// removeEntry: () => Promise<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadConfigEntrySystemOptionsDialog = () =>
|
export const loadConfigEntrySystemOptionsDialog = () =>
|
||||||
|
@@ -108,12 +108,28 @@ export const showOptionsFlowDialog = (
|
|||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormProgressHeader(_hass, _step) {
|
renderShowFormProgressHeader(hass, step) {
|
||||||
return "";
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.options.step.${step.step_id}.title`
|
||||||
|
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormProgressDescription(_hass, _step) {
|
renderShowFormProgressDescription(hass, step) {
|
||||||
return "";
|
const description = hass.localize(
|
||||||
|
`component.${configEntry.domain}.options.progress.${step.progress_action}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
return description
|
||||||
|
? html`
|
||||||
|
<ha-markdown
|
||||||
|
allowsvg
|
||||||
|
breaks
|
||||||
|
.content=${description}
|
||||||
|
></ha-markdown>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -50,6 +50,9 @@ class HaStoreAuth extends LitElement {
|
|||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
transition: bottom 0.25s;
|
transition: bottom 0.25s;
|
||||||
|
--ha-card-box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
|
||||||
|
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
|
@@ -23,16 +23,12 @@ class MoreInfoPerson extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-attributes
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${this.stateObj}
|
|
||||||
extra-filters="id,user_id,editable"
|
|
||||||
></ha-attributes>
|
|
||||||
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
|
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
|
||||||
? html`
|
? html`
|
||||||
<ha-map
|
<ha-map
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entities=${this._entityArray(this.stateObj.entity_id)}
|
.entities=${this._entityArray(this.stateObj.entity_id)}
|
||||||
|
autoFit
|
||||||
></ha-map>
|
></ha-map>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -51,6 +47,11 @@ class MoreInfoPerson extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
<ha-attributes
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
extra-filters="id,user_id,editable"
|
||||||
|
></ha-attributes>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,11 +17,6 @@ class MoreInfoTimer extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-attributes
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${this.stateObj}
|
|
||||||
extra-filters="remaining"
|
|
||||||
></ha-attributes>
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
|
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
|
||||||
? html`
|
? html`
|
||||||
@@ -57,6 +52,11 @@ class MoreInfoTimer extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
|
<ha-attributes
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
extra-filters="remaining"
|
||||||
|
></ha-attributes>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
this.stateObj.attributes.pressure,
|
this.stateObj.attributes.pressure,
|
||||||
this.hass.locale
|
this.hass.locale
|
||||||
)}
|
)}
|
||||||
${getWeatherUnit(this.hass, "air_pressure")}
|
${getWeatherUnit(this.hass, "pressure")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
@@ -5,6 +5,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
|||||||
import { throttle } from "../../common/util/throttle";
|
import { throttle } from "../../common/util/throttle";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/state-history-charts";
|
import "../../components/state-history-charts";
|
||||||
|
import { fetchUsers } from "../../data/user";
|
||||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
||||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook";
|
||||||
@@ -22,10 +23,12 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
|
|
||||||
@state() private _traceContexts?: TraceContexts;
|
@state() private _traceContexts?: TraceContexts;
|
||||||
|
|
||||||
@state() private _persons = {};
|
@state() private _userIdToName = {};
|
||||||
|
|
||||||
private _lastLogbookDate?: Date;
|
private _lastLogbookDate?: Date;
|
||||||
|
|
||||||
|
private _fetchUserPromise?: Promise<void>;
|
||||||
|
|
||||||
private _throttleGetLogbookEntries = throttle(() => {
|
private _throttleGetLogbookEntries = throttle(() => {
|
||||||
this._getLogBookData();
|
this._getLogBookData();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -59,7 +62,7 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${this._logbookEntries}
|
.entries=${this._logbookEntries}
|
||||||
.traceContexts=${this._traceContexts}
|
.traceContexts=${this._traceContexts}
|
||||||
.userIdToName=${this._persons}
|
.userIdToName=${this._userIdToName}
|
||||||
></ha-logbook>
|
></ha-logbook>
|
||||||
`
|
`
|
||||||
: html`<div class="no-entries">
|
: html`<div class="no-entries">
|
||||||
@@ -70,7 +73,7 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
this._fetchPersonNames();
|
this._fetchUserPromise = this._fetchUserNames();
|
||||||
this.addEventListener("click", (ev) => {
|
this.addEventListener("click", (ev) => {
|
||||||
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
|
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
|
||||||
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
|
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
|
||||||
@@ -125,6 +128,7 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
true
|
true
|
||||||
),
|
),
|
||||||
loadTraceContexts(this.hass),
|
loadTraceContexts(this.hass),
|
||||||
|
this._fetchUserPromise,
|
||||||
]);
|
]);
|
||||||
this._logbookEntries = this._logbookEntries
|
this._logbookEntries = this._logbookEntries
|
||||||
? [...newEntries, ...this._logbookEntries]
|
? [...newEntries, ...this._logbookEntries]
|
||||||
@@ -133,16 +137,34 @@ export class MoreInfoLogbook extends LitElement {
|
|||||||
this._traceContexts = traceContexts;
|
this._traceContexts = traceContexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fetchPersonNames() {
|
private async _fetchUserNames() {
|
||||||
|
const userIdToName = {};
|
||||||
|
|
||||||
|
// Start loading users
|
||||||
|
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||||
|
|
||||||
|
// Process persons
|
||||||
Object.values(this.hass.states).forEach((entity) => {
|
Object.values(this.hass.states).forEach((entity) => {
|
||||||
if (
|
if (
|
||||||
entity.attributes.user_id &&
|
entity.attributes.user_id &&
|
||||||
computeStateDomain(entity) === "person"
|
computeStateDomain(entity) === "person"
|
||||||
) {
|
) {
|
||||||
this._persons[entity.attributes.user_id] =
|
this._userIdToName[entity.attributes.user_id] =
|
||||||
entity.attributes.friendly_name;
|
entity.attributes.friendly_name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process users
|
||||||
|
if (userProm) {
|
||||||
|
const users = await userProm;
|
||||||
|
for (const user of users) {
|
||||||
|
if (!(user.id in userIdToName)) {
|
||||||
|
userIdToName[user.id] = user.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._userIdToName = userIdToName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import type { List } from "@material/mwc-list/mwc-list";
|
import type { List } from "@material/mwc-list/mwc-list";
|
||||||
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
||||||
@@ -188,7 +188,6 @@ export class QuickBar extends LitElement {
|
|||||||
${scroll({
|
${scroll({
|
||||||
items,
|
items,
|
||||||
layout: Layout1d,
|
layout: Layout1d,
|
||||||
// @ts-expect-error
|
|
||||||
renderItem: (item: QuickBarItem, index) =>
|
renderItem: (item: QuickBarItem, index) =>
|
||||||
this._renderItem(item, index),
|
this._renderItem(item, index),
|
||||||
})}
|
})}
|
||||||
@@ -224,7 +223,7 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
private _renderItem(item: QuickBarItem, index?: number) {
|
private _renderItem(item: QuickBarItem, index?: number) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return undefined;
|
return html``;
|
||||||
}
|
}
|
||||||
return isCommandItem(item)
|
return isCommandItem(item)
|
||||||
? this._renderCommandItem(item, index)
|
? this._renderCommandItem(item, index)
|
||||||
@@ -639,18 +638,6 @@ export class QuickBar extends LitElement {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uni-virtualizer-host {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
contain: strict;
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uni-virtualizer-host > * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
mwc-list-item {
|
mwc-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@@ -48,6 +48,9 @@
|
|||||||
window.providersPromise = fetch("/auth/providers", {
|
window.providersPromise = fetch("/auth/providers", {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
});
|
});
|
||||||
|
if (!window.globalThis) {
|
||||||
|
window.globalThis = window;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@@ -71,6 +71,9 @@
|
|||||||
import("<%= latestAppJS %>");
|
import("<%= latestAppJS %>");
|
||||||
window.customPanelJS = "<%= latestCustomPanelJS %>";
|
window.customPanelJS = "<%= latestCustomPanelJS %>";
|
||||||
window.latestJS = true;
|
window.latestJS = true;
|
||||||
|
if (!window.globalThis) {
|
||||||
|
window.globalThis = window;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
{% for extra_module in extra_modules -%}
|
{% for extra_module in extra_modules -%}
|
||||||
|
@@ -80,6 +80,9 @@
|
|||||||
window.stepsPromise = fetch("/api/onboarding", {
|
window.stepsPromise = fetch("/api/onboarding", {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
});
|
});
|
||||||
|
if (!window.globalThis) {
|
||||||
|
window.globalThis = window;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@@ -229,7 +229,7 @@ class HassTabsSubpage extends LitElement {
|
|||||||
color: var(--sidebar-text-color);
|
color: var(--sidebar-text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
:host([narrow]) .toolbar a {
|
.bottom-bar a {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,7 +12,10 @@ import { HASSDomEvent } from "../common/dom/fire_event";
|
|||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
import { subscribeOne } from "../common/util/subscribe-one";
|
import { subscribeOne } from "../common/util/subscribe-one";
|
||||||
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
|
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
|
||||||
import { fetchDiscoveryInformation } from "../data/discovery";
|
import {
|
||||||
|
DiscoveryInformation,
|
||||||
|
fetchDiscoveryInformation,
|
||||||
|
} from "../data/discovery";
|
||||||
import {
|
import {
|
||||||
fetchOnboardingOverview,
|
fetchOnboardingOverview,
|
||||||
OnboardingResponses,
|
OnboardingResponses,
|
||||||
@@ -68,6 +71,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
|
|
||||||
@state() private _steps?: OnboardingStep[];
|
@state() private _steps?: OnboardingStep[];
|
||||||
|
|
||||||
|
@state() private _discoveryInformation?: DiscoveryInformation;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const step = this._curStep()!;
|
const step = this._curStep()!;
|
||||||
|
|
||||||
@@ -87,6 +92,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
? html`<onboarding-restore-snapshot
|
? html`<onboarding-restore-snapshot
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
.restoring=${this._restoring}
|
.restoring=${this._restoring}
|
||||||
|
.discoveryInformation=${this._discoveryInformation}
|
||||||
@restoring=${this._restoringSnapshot}
|
@restoring=${this._restoringSnapshot}
|
||||||
>
|
>
|
||||||
</onboarding-restore-snapshot>`
|
</onboarding-restore-snapshot>`
|
||||||
|
@@ -5,9 +5,11 @@ import "@polymer/paper-radio-button/paper-radio-button";
|
|||||||
import "@polymer/paper-radio-group/paper-radio-group";
|
import "@polymer/paper-radio-group/paper-radio-group";
|
||||||
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, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/map/ha-location-editor";
|
import "../components/map/ha-locations-editor";
|
||||||
|
import type { MarkerLocation } from "../components/map/ha-locations-editor";
|
||||||
import { createTimezoneListEl } from "../components/timezone-datalist";
|
import { createTimezoneListEl } from "../components/timezone-datalist";
|
||||||
import {
|
import {
|
||||||
ConfigUpdateValues,
|
ConfigUpdateValues,
|
||||||
@@ -81,14 +83,14 @@ class OnboardingCoreConfig extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ha-location-editor
|
<ha-locations-editor
|
||||||
class="flex"
|
class="flex"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.location=${this._locationValue}
|
.locations=${this._markerLocation(this._locationValue)}
|
||||||
.fitZoom=${14}
|
zoom="14"
|
||||||
.darkMode=${mql.matches}
|
.darkMode=${mql.matches}
|
||||||
@change=${this._locationChanged}
|
@location-updated=${this._locationChanged}
|
||||||
></ha-location-editor>
|
></ha-locations-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -208,13 +210,24 @@ class OnboardingCoreConfig extends LitElement {
|
|||||||
return this._unitSystem !== undefined ? this._unitSystem : "metric";
|
return this._unitSystem !== undefined ? this._unitSystem : "metric";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _markerLocation = memoizeOne(
|
||||||
|
(location: [number, number]): MarkerLocation[] => [
|
||||||
|
{
|
||||||
|
id: "location",
|
||||||
|
latitude: location[0],
|
||||||
|
longitude: location[1],
|
||||||
|
location_editable: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||||
const target = ev.currentTarget as PaperInputElement;
|
const target = ev.currentTarget as PaperInputElement;
|
||||||
this[`_${target.name}`] = target.value;
|
this[`_${target.name}`] = target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _locationChanged(ev) {
|
private _locationChanged(ev) {
|
||||||
this._location = ev.currentTarget.location;
|
this._location = ev.detail.location;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _unitSystemChanged(
|
private _unitSystemChanged(
|
||||||
|
@@ -4,9 +4,12 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||||
import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
|
import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
|
||||||
import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
|
import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
|
||||||
import { navigate } from "../common/navigate";
|
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-card";
|
import "../components/ha-card";
|
||||||
|
import {
|
||||||
|
DiscoveryInformation,
|
||||||
|
fetchDiscoveryInformation,
|
||||||
|
} from "../data/discovery";
|
||||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
@@ -26,6 +29,9 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public restoring = false;
|
@property({ type: Boolean }) public restoring = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public discoveryInformation?: DiscoveryInformation;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return this.restoring
|
return this.restoring
|
||||||
? html`<ha-card
|
? html`<ha-card
|
||||||
@@ -58,13 +64,14 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
|||||||
private async _checkRestoreStatus(): Promise<void> {
|
private async _checkRestoreStatus(): Promise<void> {
|
||||||
if (this.restoring) {
|
if (this.restoring) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/hassio/supervisor/info", {
|
const response = await fetchDiscoveryInformation();
|
||||||
method: "GET",
|
|
||||||
});
|
if (
|
||||||
if (response.status === 401) {
|
!this.discoveryInformation ||
|
||||||
// If we get a unauthorized response, the restore is done
|
this.discoveryInformation.uuid !== response.uuid
|
||||||
navigate("/", { replace: true });
|
) {
|
||||||
location.reload();
|
// When the UUID changes, the restore is complete
|
||||||
|
window.location.replace("/");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// We fully expected issues with fetching info untill restore is complete.
|
// We fully expected issues with fetching info untill restore is complete.
|
||||||
@@ -76,6 +83,7 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
|
|||||||
showHassioSnapshotDialog(this, {
|
showHassioSnapshotDialog(this, {
|
||||||
slug,
|
slug,
|
||||||
onboarding: true,
|
onboarding: true,
|
||||||
|
localize: this.localize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,7 +8,8 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { UNIT_C } from "../../../common/const";
|
import { UNIT_C } from "../../../common/const";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/map/ha-location-editor";
|
import "../../../components/map/ha-locations-editor";
|
||||||
|
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||||
@@ -20,13 +21,13 @@ class ConfigCoreForm extends LitElement {
|
|||||||
|
|
||||||
@state() private _working = false;
|
@state() private _working = false;
|
||||||
|
|
||||||
@state() private _location!: [number, number];
|
@state() private _location?: [number, number];
|
||||||
|
|
||||||
@state() private _elevation!: string;
|
@state() private _elevation?: string;
|
||||||
|
|
||||||
@state() private _unitSystem!: ConfigUpdateValues["unit_system"];
|
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||||
|
|
||||||
@state() private _timeZone!: string;
|
@state() private _timeZone?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const canEdit = ["storage", "default"].includes(
|
const canEdit = ["storage", "default"].includes(
|
||||||
@@ -52,16 +53,16 @@ class ConfigCoreForm extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ha-location-editor
|
<ha-locations-editor
|
||||||
class="flex"
|
class="flex"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.location=${this._locationValue(
|
.locations=${this._markerLocation(
|
||||||
this._location,
|
|
||||||
this.hass.config.latitude,
|
this.hass.config.latitude,
|
||||||
this.hass.config.longitude
|
this.hass.config.longitude,
|
||||||
|
this._location
|
||||||
)}
|
)}
|
||||||
@change=${this._locationChanged}
|
@location-updated=${this._locationChanged}
|
||||||
></ha-location-editor>
|
></ha-locations-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -162,8 +163,19 @@ class ConfigCoreForm extends LitElement {
|
|||||||
input.inputElement.appendChild(createTimezoneListEl());
|
input.inputElement.appendChild(createTimezoneListEl());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _locationValue = memoizeOne(
|
private _markerLocation = memoizeOne(
|
||||||
(location, lat, lng) => location || [Number(lat), Number(lng)]
|
(
|
||||||
|
lat: number,
|
||||||
|
lng: number,
|
||||||
|
location?: [number, number]
|
||||||
|
): MarkerLocation[] => [
|
||||||
|
{
|
||||||
|
id: "location",
|
||||||
|
latitude: location ? location[0] : lat,
|
||||||
|
longitude: location ? location[1] : lng,
|
||||||
|
location_editable: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
private get _elevationValue() {
|
private get _elevationValue() {
|
||||||
@@ -192,7 +204,7 @@ class ConfigCoreForm extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _locationChanged(ev) {
|
private _locationChanged(ev) {
|
||||||
this._location = ev.currentTarget.location;
|
this._location = ev.detail.location;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _unitSystemChanged(
|
private _unitSystemChanged(
|
||||||
@@ -204,11 +216,10 @@ class ConfigCoreForm extends LitElement {
|
|||||||
private async _save() {
|
private async _save() {
|
||||||
this._working = true;
|
this._working = true;
|
||||||
try {
|
try {
|
||||||
const location = this._locationValue(
|
const location = this._location || [
|
||||||
this._location,
|
|
||||||
this.hass.config.latitude,
|
this.hass.config.latitude,
|
||||||
this.hass.config.longitude
|
this.hass.config.longitude,
|
||||||
);
|
];
|
||||||
await saveCoreConfig(this.hass, {
|
await saveCoreConfig(this.hass, {
|
||||||
latitude: location[0],
|
latitude: location[0],
|
||||||
longitude: location[1],
|
longitude: location[1],
|
||||||
|
@@ -9,13 +9,14 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import "../../../components/ha-network";
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-checkbox";
|
import "../../../components/ha-checkbox";
|
||||||
|
import "../../../components/ha-network";
|
||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
|
import { fetchNetworkInfo } from "../../../data/hassio/network";
|
||||||
import {
|
import {
|
||||||
NetworkConfig,
|
|
||||||
getNetworkConfig,
|
getNetworkConfig,
|
||||||
|
NetworkConfig,
|
||||||
setNetworkConfig,
|
setNetworkConfig,
|
||||||
} from "../../../data/network";
|
} from "../../../data/network";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@@ -73,7 +74,19 @@ class ConfigNetwork extends LitElement {
|
|||||||
private async _load() {
|
private async _load() {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
try {
|
try {
|
||||||
this._networkConfig = await getNetworkConfig(this.hass);
|
const coreNetwork = await getNetworkConfig(this.hass);
|
||||||
|
if (isComponentLoaded(this.hass, "hassio")) {
|
||||||
|
const supervisorNetwork = await fetchNetworkInfo(this.hass);
|
||||||
|
const interfaces = new Set(
|
||||||
|
supervisorNetwork.interfaces.map((int) => int.interface)
|
||||||
|
);
|
||||||
|
if (interfaces.size) {
|
||||||
|
coreNetwork.adapters = coreNetwork.adapters.filter((adapter) =>
|
||||||
|
interfaces.has(adapter.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._networkConfig = coreNetwork;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err.message || err;
|
this._error = err.message || err;
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,11 @@ import { slugify } from "../../../common/string/slugify";
|
|||||||
import "../../../components/entity/ha-battery-icon";
|
import "../../../components/entity/ha-battery-icon";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
|
import {
|
||||||
|
ConfigEntry,
|
||||||
|
disableConfigEntry,
|
||||||
|
DisableConfigEntryResult,
|
||||||
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
@@ -25,7 +29,10 @@ import {
|
|||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import { SceneEntities, showSceneEditor } from "../../../data/scene";
|
import { SceneEntities, showSceneEditor } from "../../../data/scene";
|
||||||
import { findRelated, RelatedResult } from "../../../data/search";
|
import { findRelated, RelatedResult } from "../../../data/search";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-error-screen";
|
import "../../../layouts/hass-error-screen";
|
||||||
import "../../../layouts/hass-tabs-subpage";
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@@ -671,13 +678,41 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
dismissText: this.hass.localize("ui.common.no"),
|
dismissText: this.hass.localize("ui.common.no"),
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
disableConfigEntry(this.hass, cnfg_entry);
|
let result: DisableConfigEntryResult;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
result = await disableConfigEntry(this.hass, cnfg_entry);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.require_restart) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
delete updates.disabled_by;
|
delete updates.disabled_by;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
|
try {
|
||||||
|
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.devices.update_device_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!oldDeviceName ||
|
!oldDeviceName ||
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
deleteConfigEntry,
|
deleteConfigEntry,
|
||||||
disableConfigEntry,
|
disableConfigEntry,
|
||||||
|
DisableConfigEntryResult,
|
||||||
enableConfigEntry,
|
enableConfigEntry,
|
||||||
reloadConfigEntry,
|
reloadConfigEntry,
|
||||||
updateConfigEntry,
|
updateConfigEntry,
|
||||||
@@ -110,6 +111,7 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
: undefined}
|
: undefined}
|
||||||
.localizedDomainName=${item ? item.localized_domain_name : undefined}
|
.localizedDomainName=${item ? item.localized_domain_name : undefined}
|
||||||
.manifest=${this.manifest}
|
.manifest=${this.manifest}
|
||||||
|
.configEntry=${item}
|
||||||
>
|
>
|
||||||
${this.items.length > 1
|
${this.items.length > 1
|
||||||
? html`
|
? html`
|
||||||
@@ -466,6 +468,11 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
private _showSystemOptions(configEntry: ConfigEntry) {
|
private _showSystemOptions(configEntry: ConfigEntry) {
|
||||||
showConfigEntrySystemOptionsDialog(this, {
|
showConfigEntrySystemOptionsDialog(this, {
|
||||||
entry: configEntry,
|
entry: configEntry,
|
||||||
|
manifest: this.manifest,
|
||||||
|
entryUpdated: (entry) =>
|
||||||
|
fireEvent(this, "entry-updated", {
|
||||||
|
entry,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +488,18 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await disableConfigEntry(this.hass, entryId);
|
let result: DisableConfigEntryResult;
|
||||||
|
try {
|
||||||
|
result = await disableConfigEntry(this.hass, entryId);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result.require_restart) {
|
if (result.require_restart) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
@@ -497,7 +515,18 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
private async _enableIntegration(configEntry: ConfigEntry) {
|
private async _enableIntegration(configEntry: ConfigEntry) {
|
||||||
const entryId = configEntry.entry_id;
|
const entryId = configEntry.entry_id;
|
||||||
|
|
||||||
const result = await enableConfigEntry(this.hass, entryId);
|
let result: DisableConfigEntryResult;
|
||||||
|
try {
|
||||||
|
result = await enableConfigEntry(this.hass, entryId);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.require_restart) {
|
if (result.require_restart) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
@@ -561,10 +590,10 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
if (newName === null) {
|
if (newName === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
|
const result = await updateConfigEntry(this.hass, configEntry.entry_id, {
|
||||||
title: newName,
|
title: newName,
|
||||||
});
|
});
|
||||||
fireEvent(this, "entry-updated", { entry: newEntry });
|
fireEvent(this, "entry-updated", { entry: result.config_entry });
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
import { css, html, LitElement, TemplateResult } from "lit";
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { ConfigEntry } from "../../../data/config_entries";
|
||||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { brandsUrl } from "../../../util/brands-url";
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
@@ -21,6 +22,8 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public configEntry?: ConfigEntry;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
let primary: string;
|
let primary: string;
|
||||||
let secondary: string | undefined;
|
let secondary: string | undefined;
|
||||||
@@ -59,6 +62,15 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.configEntry?.pref_disable_polling) {
|
||||||
|
icons.push([
|
||||||
|
mdiSyncOff,
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disabled_polling"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@@ -9,7 +9,12 @@ import {
|
|||||||
} from "../../../../../data/zha";
|
} from "../../../../../data/zha";
|
||||||
import "../../../../../layouts/hass-tabs-subpage";
|
import "../../../../../layouts/hass-tabs-subpage";
|
||||||
import type { HomeAssistant, Route } from "../../../../../types";
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
import { Network, Edge, Node, EdgeOptions } from "vis-network/peer";
|
import {
|
||||||
|
Network,
|
||||||
|
Edge,
|
||||||
|
Node,
|
||||||
|
EdgeOptions,
|
||||||
|
} from "vis-network/peer/esm/vis-network";
|
||||||
import "../../../../../common/search/search-input";
|
import "../../../../../common/search/search-input";
|
||||||
import "../../../../../components/device/ha-device-picker";
|
import "../../../../../components/device/ha-device-picker";
|
||||||
import "../../../../../components/ha-button-menu";
|
import "../../../../../components/ha-button-menu";
|
||||||
@@ -21,6 +26,7 @@ import "../../../../../components/ha-checkbox";
|
|||||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||||
import { zhaTabs } from "./zha-config-dashboard";
|
import { zhaTabs } from "./zha-config-dashboard";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import "../../../../../components/ha-formfield";
|
||||||
|
|
||||||
@customElement("zha-network-visualization-page")
|
@customElement("zha-network-visualization-page")
|
||||||
export class ZHANetworkVisualizationPage extends LitElement {
|
export class ZHANetworkVisualizationPage extends LitElement {
|
||||||
@@ -28,7 +34,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public route!: Route;
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow!: boolean;
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
@property({ type: Boolean }) public isWide!: boolean;
|
@property({ type: Boolean }) public isWide!: boolean;
|
||||||
|
|
||||||
@@ -67,8 +73,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
height: window.innerHeight + "px",
|
|
||||||
width: window.innerWidth + "px",
|
|
||||||
layout: {
|
layout: {
|
||||||
improvedLayout: true,
|
improvedLayout: true,
|
||||||
},
|
},
|
||||||
@@ -135,17 +139,35 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
"ui.panel.config.zha.visualization.header"
|
"ui.panel.config.zha.visualization.header"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="table-header">
|
${this.narrow
|
||||||
<search-input
|
? html`
|
||||||
no-label-float
|
<div slot="header">
|
||||||
no-underline
|
<search-input
|
||||||
@value-changed=${this._handleSearchChange}
|
no-label-float
|
||||||
.filter=${this._filter}
|
no-underline
|
||||||
.label=${this.hass.localize(
|
class="header"
|
||||||
"ui.panel.config.zha.visualization.highlight_label"
|
@value-changed=${this._handleSearchChange}
|
||||||
)}
|
.filter=${this._filter}
|
||||||
>
|
.label=${this.hass.localize(
|
||||||
</search-input>
|
"ui.panel.config.zha.visualization.highlight_label"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</search-input>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="header">
|
||||||
|
${!this.narrow
|
||||||
|
? html`<search-input
|
||||||
|
no-label-float
|
||||||
|
no-underline
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
.filter=${this._filter}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.zha.visualization.highlight_label"
|
||||||
|
)}
|
||||||
|
></search-input>`
|
||||||
|
: ""}
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.zoomedDeviceId}
|
.value=${this.zoomedDeviceId}
|
||||||
@@ -155,16 +177,24 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||||
@value-changed=${this._onZoomToDevice}
|
@value-changed=${this._onZoomToDevice}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
<ha-checkbox
|
<div class="controls">
|
||||||
@change=${this._handleCheckboxChange}
|
<ha-formfield
|
||||||
.checked=${this._autoZoom}
|
.label=${this.hass!.localize(
|
||||||
></ha-checkbox
|
"ui.panel.config.zha.visualization.auto_zoom"
|
||||||
>${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")}
|
)}
|
||||||
<mwc-button @click=${this._refreshTopology}
|
>
|
||||||
>${this.hass!.localize(
|
<ha-checkbox
|
||||||
"ui.panel.config.zha.visualization.refresh_topology"
|
@change=${this._handleCheckboxChange}
|
||||||
)}</mwc-button
|
.checked=${this._autoZoom}
|
||||||
>
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
</ha-formfield>
|
||||||
|
<mwc-button @click=${this._refreshTopology}>
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.zha.visualization.refresh_topology"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="visualization"></div>
|
<div id="visualization"></div>
|
||||||
</hass-tabs-subpage>
|
</hass-tabs-subpage>
|
||||||
@@ -352,30 +382,23 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
.header {
|
.header {
|
||||||
font-family: var(--paper-font-display1_-_font-family);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
-webkit-font-smoothing: var(
|
padding: 0 8px;
|
||||||
--paper-font-display1_-_-webkit-font-smoothing
|
|
||||||
);
|
|
||||||
font-size: var(--paper-font-display1_-_font-size);
|
|
||||||
font-weight: var(--paper-font-display1_-_font-weight);
|
|
||||||
letter-spacing: var(--paper-font-display1_-_letter-spacing);
|
|
||||||
line-height: var(--paper-font-display1_-_line-height);
|
|
||||||
opacity: var(--dark-primary-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-header {
|
|
||||||
border-bottom: 1px solid --divider-color;
|
|
||||||
padding: 0 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
justify-content: space-between;
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([narrow]) .table-header {
|
.header > * {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([narrow]) .header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: var(--header-height) * 3;
|
height: var(--header-height) * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-toolbar {
|
.search-toolbar {
|
||||||
@@ -386,34 +409,34 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search-input {
|
search-input {
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not([narrow])) search-input {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
search-input.header {
|
search-input.header {
|
||||||
left: -8px;
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-device-picker {
|
ha-device-picker {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
top: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not([narrow])) ha-device-picker {
|
.controls {
|
||||||
margin: 5px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-button {
|
#visualization {
|
||||||
font-weight: 500;
|
height: calc(100% - var(--header-height));
|
||||||
color: var(--primary-color);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
:host([narrow]) #visualization {
|
||||||
:host(:not([narrow])) mwc-button {
|
height: calc(100% - (var(--header-height) * 2));
|
||||||
margin: 5px;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
import "../../../../../layouts/hass-tabs-subpage";
|
import "../../../../../layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../../../resources/styles";
|
import { haStyle } from "../../../../../resources/styles";
|
||||||
import type { HomeAssistant, Route } from "../../../../../types";
|
import type { HomeAssistant, Route } from "../../../../../types";
|
||||||
|
import { fileDownload } from "../../../../../util/file_download";
|
||||||
import "../../../ha-config-section";
|
import "../../../ha-config-section";
|
||||||
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
|
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
|
||||||
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
|
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
|
||||||
@@ -312,12 +313,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = document.createElement("a");
|
fileDownload(this, signedPath.path, `zwave_js_dump.jsonl`);
|
||||||
a.href = signedPath.path;
|
|
||||||
a.download = `zwave_js_dump.jsonl`;
|
|
||||||
this.shadowRoot!.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
this.shadowRoot!.removeChild(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -9,13 +9,9 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
|||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
import "../../../components/map/ha-location-editor";
|
import "../../../components/map/ha-locations-editor";
|
||||||
import {
|
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||||
defaultRadiusColor,
|
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone";
|
||||||
getZoneEditorInitData,
|
|
||||||
passiveRadiusColor,
|
|
||||||
ZoneMutableParams,
|
|
||||||
} from "../../../data/zone";
|
|
||||||
import { haStyleDialog } from "../../../resources/styles";
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||||
@@ -132,17 +128,19 @@ class DialogZoneDetail extends LitElement {
|
|||||||
)}"
|
)}"
|
||||||
.invalid=${iconValid}
|
.invalid=${iconValid}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
<ha-location-editor
|
<ha-locations-editor
|
||||||
class="flex"
|
class="flex"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.location=${this._locationValue(this._latitude, this._longitude)}
|
.locations=${this._location(
|
||||||
.radius=${this._radius}
|
this._latitude,
|
||||||
.radiusColor=${this._passive
|
this._longitude,
|
||||||
? passiveRadiusColor
|
this._radius,
|
||||||
: defaultRadiusColor}
|
this._passive,
|
||||||
.icon=${this._icon}
|
this._icon
|
||||||
@change=${this._locationChanged}
|
)}
|
||||||
></ha-location-editor>
|
@location-updated=${this._locationChanged}
|
||||||
|
@radius-updated=${this._radiusChanged}
|
||||||
|
></ha-locations-editor>
|
||||||
<div class="location">
|
<div class="location">
|
||||||
<paper-input
|
<paper-input
|
||||||
.value=${this._latitude}
|
.value=${this._latitude}
|
||||||
@@ -222,11 +220,40 @@ class DialogZoneDetail extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _locationValue = memoizeOne((lat, lng) => [Number(lat), Number(lng)]);
|
private _location = memoizeOne(
|
||||||
|
(
|
||||||
|
lat: number,
|
||||||
|
lng: number,
|
||||||
|
radius: number,
|
||||||
|
passive: boolean,
|
||||||
|
icon: string
|
||||||
|
): MarkerLocation[] => {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
|
||||||
|
const passiveRadiusColor = computedStyles.getPropertyValue(
|
||||||
|
"--secondary-text-color"
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "location",
|
||||||
|
latitude: Number(lat),
|
||||||
|
longitude: Number(lng),
|
||||||
|
radius,
|
||||||
|
radius_color: passive ? passiveRadiusColor : zoneRadiusColor,
|
||||||
|
icon,
|
||||||
|
location_editable: true,
|
||||||
|
radius_editable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _locationChanged(ev) {
|
private _locationChanged(ev: CustomEvent) {
|
||||||
[this._latitude, this._longitude] = ev.currentTarget.location;
|
[this._latitude, this._longitude] = ev.detail.location;
|
||||||
this._radius = ev.currentTarget.radius;
|
}
|
||||||
|
|
||||||
|
private _radiusChanged(ev: CustomEvent) {
|
||||||
|
this._radius = ev.detail.radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _passiveChanged(ev) {
|
private _passiveChanged(ev) {
|
||||||
@@ -292,7 +319,7 @@ class DialogZoneDetail extends LitElement {
|
|||||||
.location > *:last-child {
|
.location > *:last-child {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
ha-location-editor {
|
ha-locations-editor {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
|
@@ -31,11 +31,8 @@ import { saveCoreConfig } from "../../../data/core";
|
|||||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
createZone,
|
createZone,
|
||||||
defaultRadiusColor,
|
|
||||||
deleteZone,
|
deleteZone,
|
||||||
fetchZones,
|
fetchZones,
|
||||||
homeRadiusColor,
|
|
||||||
passiveRadiusColor,
|
|
||||||
updateZone,
|
updateZone,
|
||||||
Zone,
|
Zone,
|
||||||
ZoneMutableParams,
|
ZoneMutableParams,
|
||||||
@@ -73,6 +70,15 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _getZones = memoizeOne(
|
private _getZones = memoizeOne(
|
||||||
(storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => {
|
(storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
|
||||||
|
const passiveRadiusColor = computedStyles.getPropertyValue(
|
||||||
|
"--secondary-text-color"
|
||||||
|
);
|
||||||
|
const homeRadiusColor = computedStyles.getPropertyValue(
|
||||||
|
"--primary-color"
|
||||||
|
);
|
||||||
|
|
||||||
const stateLocations: MarkerLocation[] = stateItems.map(
|
const stateLocations: MarkerLocation[] = stateItems.map(
|
||||||
(entityState) => ({
|
(entityState) => ({
|
||||||
id: entityState.entity_id,
|
id: entityState.entity_id,
|
||||||
@@ -86,7 +92,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
? homeRadiusColor
|
? homeRadiusColor
|
||||||
: entityState.attributes.passive
|
: entityState.attributes.passive
|
||||||
? passiveRadiusColor
|
? passiveRadiusColor
|
||||||
: defaultRadiusColor,
|
: zoneRadiusColor,
|
||||||
location_editable:
|
location_editable:
|
||||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||||
radius_editable: false,
|
radius_editable: false,
|
||||||
@@ -94,7 +100,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
|
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
|
||||||
...zone,
|
...zone,
|
||||||
radius_color: zone.passive ? passiveRadiusColor : defaultRadiusColor,
|
radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor,
|
||||||
location_editable: true,
|
location_editable: true,
|
||||||
radius_editable: true,
|
radius_editable: true,
|
||||||
}));
|
}));
|
||||||
@@ -274,7 +280,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
if (oldHass && this._stateItems) {
|
if (oldHass && this._stateItems) {
|
||||||
@@ -410,8 +416,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
if (this.narrow) {
|
if (this.narrow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.updateComplete;
|
|
||||||
this._activeEntry = created.id;
|
this._activeEntry = created.id;
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._map?.updateComplete;
|
||||||
this._map?.fitMarker(created.id);
|
this._map?.fitMarker(created.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,8 +434,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
if (this.narrow || !fitMap) {
|
if (this.narrow || !fitMap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.updateComplete;
|
|
||||||
this._activeEntry = entry.id;
|
this._activeEntry = entry.id;
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._map?.updateComplete;
|
||||||
this._map?.fitMarker(entry.id);
|
this._map?.fitMarker(entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
import { Layout1d, scroll } from "../../resources/lit-virtualizer";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -100,7 +100,6 @@ class HaLogbook extends LitElement {
|
|||||||
? scroll({
|
? scroll({
|
||||||
items: this.entries,
|
items: this.entries,
|
||||||
layout: Layout1d,
|
layout: Layout1d,
|
||||||
// @ts-expect-error
|
|
||||||
renderItem: (item: LogbookEntry, index) =>
|
renderItem: (item: LogbookEntry, index) =>
|
||||||
this._renderLogbookItem(item, index),
|
this._renderLogbookItem(item, index),
|
||||||
})
|
})
|
||||||
@@ -354,15 +353,7 @@ class HaLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host([virtualize]) .container {
|
:host([virtualize]) .container {
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
contain: strict;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container > * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.narrow .entry {
|
.narrow .entry {
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
import { mdiRefresh } from "@mdi/js";
|
import { mdiRefresh } from "@mdi/js";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
import "@polymer/app-layout/app-header/app-header";
|
import "@polymer/app-layout/app-header/app-header";
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import "../../components/entity/ha-entity-picker";
|
import "../../components/entity/ha-entity-picker";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/ha-date-range-picker";
|
import "../../components/ha-date-range-picker";
|
||||||
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
||||||
import "../../components/ha-icon-button";
|
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import {
|
import {
|
||||||
clearLogbookCache,
|
clearLogbookCache,
|
||||||
getLogbookData,
|
getLogbookData,
|
||||||
LogbookEntry,
|
LogbookEntry,
|
||||||
} from "../../data/logbook";
|
} from "../../data/logbook";
|
||||||
import { fetchPersons } from "../../data/person";
|
|
||||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||||
import { fetchUsers } from "../../data/user";
|
import { fetchUsers } from "../../data/user";
|
||||||
import "../../layouts/ha-app-layout";
|
import "../../layouts/ha-app-layout";
|
||||||
@@ -44,7 +44,7 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
|
|
||||||
@state() private _ranges?: DateRangePickerRanges;
|
@state() private _ranges?: DateRangePickerRanges;
|
||||||
|
|
||||||
private _fetchUserDone?: Promise<unknown>;
|
private _fetchUserPromise?: Promise<void>;
|
||||||
|
|
||||||
@state() private _userIdToName = {};
|
@state() private _userIdToName = {};
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this.hass.loadBackendTranslation("title");
|
this.hass.loadBackendTranslation("title");
|
||||||
|
|
||||||
this._fetchUserDone = this._fetchUserNames();
|
this._fetchUserPromise = this._fetchUserNames();
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@@ -198,23 +198,19 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
private async _fetchUserNames() {
|
private async _fetchUserNames() {
|
||||||
const userIdToName = {};
|
const userIdToName = {};
|
||||||
|
|
||||||
// Start loading all the data
|
// Start loading users
|
||||||
const personProm = fetchPersons(this.hass);
|
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||||
const userProm = this.hass.user!.is_admin && fetchUsers(this.hass);
|
|
||||||
|
|
||||||
// Process persons
|
// Process persons
|
||||||
const persons = await personProm;
|
Object.values(this.hass.states).forEach((entity) => {
|
||||||
|
if (
|
||||||
for (const person of persons.storage) {
|
entity.attributes.user_id &&
|
||||||
if (person.user_id) {
|
computeStateDomain(entity) === "person"
|
||||||
userIdToName[person.user_id] = person.name;
|
) {
|
||||||
|
this._userIdToName[entity.attributes.user_id] =
|
||||||
|
entity.attributes.friendly_name;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
for (const person of persons.config) {
|
|
||||||
if (person.user_id) {
|
|
||||||
userIdToName[person.user_id] = person.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process users
|
// Process users
|
||||||
if (userProm) {
|
if (userProm) {
|
||||||
@@ -262,7 +258,7 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
this._entityId
|
this._entityId
|
||||||
),
|
),
|
||||||
isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {},
|
isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {},
|
||||||
this._fetchUserDone,
|
this._fetchUserPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this._entries = entries;
|
this._entries = entries;
|
||||||
|
@@ -9,11 +9,12 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
|
||||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||||
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { throttle } from "../../../common/util/throttle";
|
import { throttle } from "../../../common/util/throttle";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-circular-progress";
|
import "../../../components/ha-circular-progress";
|
||||||
|
import { fetchUsers } from "../../../data/user";
|
||||||
import { getLogbookData, LogbookEntry } from "../../../data/logbook";
|
import { getLogbookData, LogbookEntry } from "../../../data/logbook";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import "../../logbook/ha-logbook";
|
import "../../logbook/ha-logbook";
|
||||||
@@ -51,18 +52,20 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _config?: LogbookCardConfig;
|
@state() private _config?: LogbookCardConfig;
|
||||||
|
|
||||||
@state() private _logbookEntries?: LogbookEntry[];
|
@state() private _logbookEntries?: LogbookEntry[];
|
||||||
|
|
||||||
@state() private _persons = {};
|
|
||||||
|
|
||||||
@state() private _configEntities?: EntityConfig[];
|
@state() private _configEntities?: EntityConfig[];
|
||||||
|
|
||||||
|
@state() private _userIdToName = {};
|
||||||
|
|
||||||
private _lastLogbookDate?: Date;
|
private _lastLogbookDate?: Date;
|
||||||
|
|
||||||
|
private _fetchUserPromise?: Promise<void>;
|
||||||
|
|
||||||
private _throttleGetLogbookEntries = throttle(() => {
|
private _throttleGetLogbookEntries = throttle(() => {
|
||||||
this._getLogBookData();
|
this._getLogBookData();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -114,7 +117,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
this._fetchPersonNames();
|
this._fetchUserPromise = this._fetchUserNames();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
@@ -199,7 +202,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
virtualize
|
virtualize
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entries=${this._logbookEntries}
|
.entries=${this._logbookEntries}
|
||||||
.userIdToName=${this._persons}
|
.userIdToName=${this._userIdToName}
|
||||||
></ha-logbook>
|
></ha-logbook>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -229,13 +232,16 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
const lastDate = this._lastLogbookDate || hoursToShowDate;
|
const lastDate = this._lastLogbookDate || hoursToShowDate;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const newEntries = await getLogbookData(
|
const [newEntries] = await Promise.all([
|
||||||
this.hass,
|
getLogbookData(
|
||||||
lastDate.toISOString(),
|
this.hass,
|
||||||
now.toISOString(),
|
lastDate.toISOString(),
|
||||||
this._configEntities!.map((entity) => entity.entity).toString(),
|
now.toISOString(),
|
||||||
true
|
this._configEntities!.map((entity) => entity.entity).toString(),
|
||||||
);
|
true
|
||||||
|
),
|
||||||
|
this._fetchUserPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
const logbookEntries = this._logbookEntries
|
const logbookEntries = this._logbookEntries
|
||||||
? [...newEntries, ...this._logbookEntries]
|
? [...newEntries, ...this._logbookEntries]
|
||||||
@@ -248,20 +254,34 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
this._lastLogbookDate = now;
|
this._lastLogbookDate = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fetchPersonNames() {
|
private async _fetchUserNames() {
|
||||||
if (!this.hass) {
|
const userIdToName = {};
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Start loading users
|
||||||
|
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||||
|
|
||||||
|
// Process persons
|
||||||
Object.values(this.hass!.states).forEach((entity) => {
|
Object.values(this.hass!.states).forEach((entity) => {
|
||||||
if (
|
if (
|
||||||
entity.attributes.user_id &&
|
entity.attributes.user_id &&
|
||||||
computeStateDomain(entity) === "person"
|
computeStateDomain(entity) === "person"
|
||||||
) {
|
) {
|
||||||
this._persons[entity.attributes.user_id] =
|
this._userIdToName[entity.attributes.user_id] =
|
||||||
entity.attributes.friendly_name;
|
entity.attributes.friendly_name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process users
|
||||||
|
if (userProm) {
|
||||||
|
const users = await userProm;
|
||||||
|
for (const user of users) {
|
||||||
|
if (!(user.id in userIdToName)) {
|
||||||
|
userIdToName[user.id] = user.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._userIdToName = userIdToName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -1,14 +1,5 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||||
import {
|
import { LatLngTuple } from "leaflet";
|
||||||
Circle,
|
|
||||||
CircleMarker,
|
|
||||||
LatLngTuple,
|
|
||||||
Layer,
|
|
||||||
Map,
|
|
||||||
Marker,
|
|
||||||
Polyline,
|
|
||||||
TileLayer,
|
|
||||||
} from "leaflet";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -17,32 +8,106 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import {
|
|
||||||
LeafletModuleType,
|
|
||||||
replaceTileLayer,
|
|
||||||
setupLeafletMap,
|
|
||||||
} from "../../../common/dom/setup-leaflet-map";
|
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
|
||||||
import { debounce } from "../../../common/util/debounce";
|
|
||||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import { fetchRecent } from "../../../data/history";
|
import { fetchRecent } from "../../../data/history";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import "../../map/ha-entity-marker";
|
import "../../../components/map/ha-entity-marker";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { installResizeObserver } from "../common/install-resize-observer";
|
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
import { LovelaceCard } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
import { MapCardConfig } from "./types";
|
import { MapCardConfig } from "./types";
|
||||||
|
import "../../../components/map/ha-map";
|
||||||
|
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||||
|
import type { HaMap, HaMapPaths } from "../../../components/map/ha-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
|
||||||
|
const MINUTE = 60000;
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
"#0288D1",
|
||||||
|
"#00AA00",
|
||||||
|
"#984ea3",
|
||||||
|
"#00d2d5",
|
||||||
|
"#ff7f00",
|
||||||
|
"#af8d00",
|
||||||
|
"#7f80cd",
|
||||||
|
"#b3e900",
|
||||||
|
"#c42e60",
|
||||||
|
"#a65628",
|
||||||
|
"#f781bf",
|
||||||
|
"#8dd3c7",
|
||||||
|
];
|
||||||
@customElement("hui-map-card")
|
@customElement("hui-map-card")
|
||||||
class HuiMapCard extends LitElement implements LovelaceCard {
|
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public isPanel = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _history?: HassEntity[][];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _config?: MapCardConfig;
|
||||||
|
|
||||||
|
@query("ha-map")
|
||||||
|
private _map?: HaMap;
|
||||||
|
|
||||||
|
private _date?: Date;
|
||||||
|
|
||||||
|
private _configEntities?: string[];
|
||||||
|
|
||||||
|
private _colorDict: Record<string, string> = {};
|
||||||
|
|
||||||
|
private _colorIndex = 0;
|
||||||
|
|
||||||
|
public setConfig(config: MapCardConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Error in card configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.entities?.length && !config.geo_location_sources) {
|
||||||
|
throw new Error(
|
||||||
|
"Either entities or geo_location_sources must be specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.entities && !Array.isArray(config.entities)) {
|
||||||
|
throw new Error("Entities need to be an array");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.geo_location_sources &&
|
||||||
|
!Array.isArray(config.geo_location_sources)
|
||||||
|
) {
|
||||||
|
throw new Error("Geo_location_sources needs to be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = config;
|
||||||
|
this._configEntities = (config.entities
|
||||||
|
? processConfigEntities<EntityConfig>(config.entities)
|
||||||
|
: []
|
||||||
|
).map((entity) => entity.entity);
|
||||||
|
|
||||||
|
this._cleanupHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardSize(): number {
|
||||||
|
if (!this._config?.aspect_ratio) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||||
|
const ar =
|
||||||
|
ratio && ratio.w > 0 && ratio.h > 0
|
||||||
|
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
|
||||||
|
: "100";
|
||||||
|
return 1 + Math.floor(Number(ar) / 25) || 3;
|
||||||
|
}
|
||||||
|
|
||||||
public static async getConfigElement() {
|
public static async getConfigElement() {
|
||||||
await import("../editor/config-elements/hui-map-card-editor");
|
await import("../editor/config-elements/hui-map-card-editor");
|
||||||
return document.createElement("hui-map-card-editor");
|
return document.createElement("hui-map-card-editor");
|
||||||
@@ -66,129 +131,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return { type: "map", entities: foundEntities };
|
return { type: "map", entities: foundEntities };
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
|
||||||
public isPanel = false;
|
|
||||||
|
|
||||||
@property()
|
|
||||||
private _history?: HassEntity[][];
|
|
||||||
|
|
||||||
private _date?: Date;
|
|
||||||
|
|
||||||
@property()
|
|
||||||
private _config?: MapCardConfig;
|
|
||||||
|
|
||||||
private _configEntities?: EntityConfig[];
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
private Leaflet?: LeafletModuleType;
|
|
||||||
|
|
||||||
private _leafletMap?: Map;
|
|
||||||
|
|
||||||
private _tileLayer?: TileLayer;
|
|
||||||
|
|
||||||
private _resizeObserver?: ResizeObserver;
|
|
||||||
|
|
||||||
private _debouncedResizeListener = debounce(
|
|
||||||
() => {
|
|
||||||
if (!this.isConnected || !this._leafletMap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._leafletMap.invalidateSize();
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
private _mapItems: Array<Marker | Circle> = [];
|
|
||||||
|
|
||||||
private _mapZones: Array<Marker | Circle> = [];
|
|
||||||
|
|
||||||
private _mapPaths: Array<Polyline | CircleMarker> = [];
|
|
||||||
|
|
||||||
private _colorDict: Record<string, string> = {};
|
|
||||||
|
|
||||||
private _colorIndex = 0;
|
|
||||||
|
|
||||||
private _colors: string[] = [
|
|
||||||
"#0288D1",
|
|
||||||
"#00AA00",
|
|
||||||
"#984ea3",
|
|
||||||
"#00d2d5",
|
|
||||||
"#ff7f00",
|
|
||||||
"#af8d00",
|
|
||||||
"#7f80cd",
|
|
||||||
"#b3e900",
|
|
||||||
"#c42e60",
|
|
||||||
"#a65628",
|
|
||||||
"#f781bf",
|
|
||||||
"#8dd3c7",
|
|
||||||
];
|
|
||||||
|
|
||||||
public setConfig(config: MapCardConfig): void {
|
|
||||||
if (!config) {
|
|
||||||
throw new Error("Error in card configuration.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.entities?.length && !config.geo_location_sources) {
|
|
||||||
throw new Error(
|
|
||||||
"Either entities or geo_location_sources must be specified"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (config.entities && !Array.isArray(config.entities)) {
|
|
||||||
throw new Error("Entities need to be an array");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.geo_location_sources &&
|
|
||||||
!Array.isArray(config.geo_location_sources)
|
|
||||||
) {
|
|
||||||
throw new Error("Geo_location_sources needs to be an array");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._config = config;
|
|
||||||
this._configEntities = config.entities
|
|
||||||
? processConfigEntities(config.entities)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
this._cleanupHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardSize(): number {
|
|
||||||
if (!this._config?.aspect_ratio) {
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
|
||||||
const ar =
|
|
||||||
ratio && ratio.w > 0 && ratio.h > 0
|
|
||||||
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
|
|
||||||
: "100";
|
|
||||||
return 1 + Math.floor(Number(ar) / 25) || 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connectedCallback(): void {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._attachObserver();
|
|
||||||
if (this.hasUpdated) {
|
|
||||||
this.loadMap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback(): void {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
|
|
||||||
if (this._leafletMap) {
|
|
||||||
this._leafletMap.remove();
|
|
||||||
this._leafletMap = undefined;
|
|
||||||
this.Leaflet = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._resizeObserver) {
|
|
||||||
this._resizeObserver.unobserve(this._mapEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -196,22 +138,29 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return html`
|
return html`
|
||||||
<ha-card id="card" .header=${this._config.title}>
|
<ha-card id="card" .header=${this._config.title}>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<div
|
<ha-map
|
||||||
id="map"
|
.hass=${this.hass}
|
||||||
class=${classMap({ dark: this._config.dark_mode === true })}
|
.entities=${this._getEntities(
|
||||||
></div>
|
this.hass.states,
|
||||||
<ha-icon-button
|
this._config,
|
||||||
|
this._configEntities
|
||||||
|
)}
|
||||||
|
.paths=${this._getHistoryPaths(this._config, this._history)}
|
||||||
|
.darkMode=${this._config.dark_mode}
|
||||||
|
></ha-map>
|
||||||
|
<mwc-icon-button
|
||||||
@click=${this._fitMap}
|
@click=${this._fitMap}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
icon="hass:image-filter-center-focus"
|
|
||||||
title="Reset focus"
|
title="Reset focus"
|
||||||
></ha-icon-button>
|
>
|
||||||
|
<ha-svg-icon .path=${mdiImageFilterCenterFocus}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected shouldUpdate(changedProps) {
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
if (!changedProps.has("hass") || changedProps.size > 1) {
|
if (!changedProps.has("hass") || changedProps.size > 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -228,7 +177,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
// Check if any state has changed
|
// Check if any state has changed
|
||||||
for (const entity of this._configEntities) {
|
for (const entity of this._configEntities) {
|
||||||
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
|
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,17 +187,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
if (this.isConnected) {
|
|
||||||
this.loadMap();
|
|
||||||
}
|
|
||||||
const root = this.shadowRoot!.getElementById("root");
|
const root = this.shadowRoot!.getElementById("root");
|
||||||
|
|
||||||
if (!this._config || this.isPanel || !root) {
|
if (!this._config || this.isPanel || !root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._attachObserver();
|
|
||||||
|
|
||||||
if (!this._config.aspect_ratio) {
|
if (!this._config.aspect_ratio) {
|
||||||
root.style.paddingBottom = "100%";
|
root.style.paddingBottom = "100%";
|
||||||
return;
|
return;
|
||||||
@@ -263,172 +207,86 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has("hass") || changedProps.has("_history")) {
|
|
||||||
this._drawEntities();
|
|
||||||
this._fitMap();
|
|
||||||
}
|
|
||||||
if (changedProps.has("hass")) {
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
|
||||||
if (oldHass && oldHass.themes.darkMode !== this.hass.themes.darkMode) {
|
|
||||||
this._replaceTileLayer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
changedProps.has("_config") &&
|
|
||||||
changedProps.get("_config") !== undefined
|
|
||||||
) {
|
|
||||||
this.updateMap(changedProps.get("_config") as MapCardConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._config?.hours_to_show && this._configEntities?.length) {
|
if (this._config?.hours_to_show && this._configEntities?.length) {
|
||||||
const minute = 60000;
|
|
||||||
if (changedProps.has("_config")) {
|
if (changedProps.has("_config")) {
|
||||||
this._getHistory();
|
this._getHistory();
|
||||||
} else if (Date.now() - this._date!.getTime() >= minute) {
|
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
||||||
this._getHistory();
|
this._getHistory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _mapEl(): HTMLDivElement {
|
private _fitMap() {
|
||||||
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
this._map?.fitMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMap(): Promise<void> {
|
private _getColor(entityId: string): string {
|
||||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
let color = this._colorDict[entityId];
|
||||||
this._mapEl,
|
if (color) {
|
||||||
this._config!.dark_mode ?? this.hass.themes.darkMode
|
return color;
|
||||||
);
|
|
||||||
this._drawEntities();
|
|
||||||
this._leafletMap.invalidateSize();
|
|
||||||
this._fitMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _replaceTileLayer() {
|
|
||||||
const map = this._leafletMap;
|
|
||||||
const config = this._config;
|
|
||||||
const Leaflet = this.Leaflet;
|
|
||||||
if (!map || !config || !Leaflet || !this._tileLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._tileLayer = replaceTileLayer(
|
|
||||||
Leaflet,
|
|
||||||
map,
|
|
||||||
this._tileLayer,
|
|
||||||
this._config!.dark_mode ?? this.hass.themes.darkMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateMap(oldConfig: MapCardConfig): void {
|
|
||||||
const map = this._leafletMap;
|
|
||||||
const config = this._config;
|
|
||||||
const Leaflet = this.Leaflet;
|
|
||||||
if (!map || !config || !Leaflet || !this._tileLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._config!.dark_mode !== oldConfig.dark_mode) {
|
|
||||||
this._replaceTileLayer();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.entities !== oldConfig.entities ||
|
|
||||||
config.geo_location_sources !== oldConfig.geo_location_sources
|
|
||||||
) {
|
|
||||||
this._drawEntities();
|
|
||||||
}
|
|
||||||
map.invalidateSize();
|
|
||||||
this._fitMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _fitMap(): void {
|
|
||||||
if (!this._leafletMap || !this.Leaflet || !this._config || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const zoom = this._config.default_zoom;
|
|
||||||
if (this._mapItems.length === 0) {
|
|
||||||
this._leafletMap.setView(
|
|
||||||
new this.Leaflet.LatLng(
|
|
||||||
this.hass.config.latitude,
|
|
||||||
this.hass.config.longitude
|
|
||||||
),
|
|
||||||
zoom || 14
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds();
|
|
||||||
this._leafletMap.fitBounds(bounds.pad(0.5));
|
|
||||||
|
|
||||||
if (zoom && this._leafletMap.getZoom() > zoom) {
|
|
||||||
this._leafletMap.setZoom(zoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getColor(entityId: string) {
|
|
||||||
let color;
|
|
||||||
if (this._colorDict[entityId]) {
|
|
||||||
color = this._colorDict[entityId];
|
|
||||||
} else {
|
|
||||||
color = this._colors[this._colorIndex];
|
|
||||||
this._colorIndex = (this._colorIndex + 1) % this._colors.length;
|
|
||||||
this._colorDict[entityId] = color;
|
|
||||||
}
|
}
|
||||||
|
color = COLORS[this._colorIndex % COLORS.length];
|
||||||
|
this._colorIndex++;
|
||||||
|
this._colorDict[entityId] = color;
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _drawEntities(): void {
|
private _getEntities = memoizeOne(
|
||||||
const hass = this.hass;
|
(
|
||||||
const map = this._leafletMap;
|
states: HassEntities,
|
||||||
const config = this._config;
|
config: MapCardConfig,
|
||||||
const Leaflet = this.Leaflet;
|
configEntities?: string[]
|
||||||
if (!hass || !map || !config || !Leaflet) {
|
) => {
|
||||||
return;
|
if (!states || !config) {
|
||||||
}
|
return undefined;
|
||||||
|
|
||||||
if (this._mapItems) {
|
|
||||||
this._mapItems.forEach((marker) => marker.remove());
|
|
||||||
}
|
|
||||||
const mapItems: Layer[] = (this._mapItems = []);
|
|
||||||
|
|
||||||
if (this._mapZones) {
|
|
||||||
this._mapZones.forEach((marker) => marker.remove());
|
|
||||||
}
|
|
||||||
const mapZones: Layer[] = (this._mapZones = []);
|
|
||||||
|
|
||||||
if (this._mapPaths) {
|
|
||||||
this._mapPaths.forEach((marker) => marker.remove());
|
|
||||||
}
|
|
||||||
const mapPaths: Layer[] = (this._mapPaths = []);
|
|
||||||
|
|
||||||
const allEntities = this._configEntities!.concat();
|
|
||||||
|
|
||||||
// Calculate visible geo location sources
|
|
||||||
if (config.geo_location_sources) {
|
|
||||||
const includesAll = config.geo_location_sources.includes("all");
|
|
||||||
for (const entityId of Object.keys(hass.states)) {
|
|
||||||
const stateObj = hass.states[entityId];
|
|
||||||
if (
|
|
||||||
computeDomain(entityId) === "geo_location" &&
|
|
||||||
(includesAll ||
|
|
||||||
config.geo_location_sources.includes(stateObj.attributes.source))
|
|
||||||
) {
|
|
||||||
allEntities.push({ entity: entityId });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// DRAW history
|
let entities = configEntities || [];
|
||||||
if (this._config!.hours_to_show && this._history) {
|
|
||||||
for (const entityStates of this._history) {
|
if (config.geo_location_sources) {
|
||||||
|
const geoEntities: string[] = [];
|
||||||
|
// Calculate visible geo location sources
|
||||||
|
const includesAll = config.geo_location_sources.includes("all");
|
||||||
|
for (const stateObj of Object.values(states)) {
|
||||||
|
if (
|
||||||
|
computeDomain(stateObj.entity_id) === "geo_location" &&
|
||||||
|
(includesAll ||
|
||||||
|
config.geo_location_sources.includes(stateObj.attributes.source))
|
||||||
|
) {
|
||||||
|
geoEntities.push(stateObj.entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = [...entities, ...geoEntities];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities.map((entity) => ({
|
||||||
|
entity_id: entity,
|
||||||
|
color: this._getColor(entity),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getHistoryPaths = memoizeOne(
|
||||||
|
(
|
||||||
|
config: MapCardConfig,
|
||||||
|
history?: HassEntity[][]
|
||||||
|
): HaMapPaths[] | undefined => {
|
||||||
|
if (!config.hours_to_show || !history) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths: HaMapPaths[] = [];
|
||||||
|
|
||||||
|
for (const entityStates of history) {
|
||||||
if (entityStates?.length <= 1) {
|
if (entityStates?.length <= 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entityId = entityStates[0].entity_id;
|
|
||||||
|
|
||||||
// filter location data from states and remove all invalid locations
|
// filter location data from states and remove all invalid locations
|
||||||
const path = entityStates.reduce(
|
const points = entityStates.reduce(
|
||||||
(accumulator: LatLngTuple[], state) => {
|
(accumulator: LatLngTuple[], entityState) => {
|
||||||
const latitude = state.attributes.latitude;
|
const latitude = entityState.attributes.latitude;
|
||||||
const longitude = state.attributes.longitude;
|
const longitude = entityState.attributes.longitude;
|
||||||
if (latitude && longitude) {
|
if (latitude && longitude) {
|
||||||
accumulator.push([latitude, longitude] as LatLngTuple);
|
accumulator.push([latitude, longitude] as LatLngTuple);
|
||||||
}
|
}
|
||||||
@@ -437,162 +295,15 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
[]
|
[]
|
||||||
) as LatLngTuple[];
|
) as LatLngTuple[];
|
||||||
|
|
||||||
// DRAW HISTORY
|
paths.push({
|
||||||
for (
|
points,
|
||||||
let markerIndex = 0;
|
color: this._getColor(entityStates[0].entity_id),
|
||||||
markerIndex < path.length - 1;
|
gradualOpacity: 0.8,
|
||||||
markerIndex++
|
});
|
||||||
) {
|
|
||||||
const opacityStep = 0.8 / (path.length - 2);
|
|
||||||
const opacity = 0.2 + markerIndex * opacityStep;
|
|
||||||
|
|
||||||
// DRAW history path dots
|
|
||||||
mapPaths.push(
|
|
||||||
Leaflet.circleMarker(path[markerIndex], {
|
|
||||||
radius: 3,
|
|
||||||
color: this._getColor(entityId),
|
|
||||||
opacity,
|
|
||||||
interactive: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// DRAW history path lines
|
|
||||||
const line = [path[markerIndex], path[markerIndex + 1]];
|
|
||||||
mapPaths.push(
|
|
||||||
Leaflet.polyline(line, {
|
|
||||||
color: this._getColor(entityId),
|
|
||||||
opacity,
|
|
||||||
interactive: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return paths;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// DRAW entities
|
|
||||||
for (const entity of allEntities) {
|
|
||||||
const entityId = entity.entity;
|
|
||||||
const stateObj = hass.states[entityId];
|
|
||||||
if (!stateObj) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const title = computeStateName(stateObj);
|
|
||||||
const {
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
passive,
|
|
||||||
icon,
|
|
||||||
radius,
|
|
||||||
entity_picture: entityPicture,
|
|
||||||
gps_accuracy: gpsAccuracy,
|
|
||||||
} = stateObj.attributes;
|
|
||||||
|
|
||||||
if (!(latitude && longitude)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (computeStateDomain(stateObj) === "zone") {
|
|
||||||
// DRAW ZONE
|
|
||||||
if (passive) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create icon
|
|
||||||
let iconHTML = "";
|
|
||||||
if (icon) {
|
|
||||||
const el = document.createElement("ha-icon");
|
|
||||||
el.setAttribute("icon", icon);
|
|
||||||
iconHTML = el.outerHTML;
|
|
||||||
} else {
|
|
||||||
const el = document.createElement("span");
|
|
||||||
el.innerHTML = title;
|
|
||||||
iconHTML = el.outerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create marker with the icon
|
|
||||||
mapZones.push(
|
|
||||||
Leaflet.marker([latitude, longitude], {
|
|
||||||
icon: Leaflet.divIcon({
|
|
||||||
html: iconHTML,
|
|
||||||
iconSize: [24, 24],
|
|
||||||
className: this._config!.dark_mode
|
|
||||||
? "dark"
|
|
||||||
: this._config!.dark_mode === false
|
|
||||||
? "light"
|
|
||||||
: "",
|
|
||||||
}),
|
|
||||||
interactive: false,
|
|
||||||
title,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around it
|
|
||||||
mapZones.push(
|
|
||||||
Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
|
||||||
color: "#FF9800",
|
|
||||||
radius,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAW ENTITY
|
|
||||||
// create icon
|
|
||||||
const entityName = title
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part[0])
|
|
||||||
.join("")
|
|
||||||
.substr(0, 3);
|
|
||||||
|
|
||||||
// create market with the icon
|
|
||||||
mapItems.push(
|
|
||||||
Leaflet.marker([latitude, longitude], {
|
|
||||||
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: `
|
|
||||||
<ha-entity-marker
|
|
||||||
entity-id="${entityId}"
|
|
||||||
entity-name="${entityName}"
|
|
||||||
entity-picture="${entityPicture || ""}"
|
|
||||||
entity-color="${this._getColor(entityId)}"
|
|
||||||
></ha-entity-marker>
|
|
||||||
`,
|
|
||||||
iconSize: [48, 48],
|
|
||||||
className: "",
|
|
||||||
}),
|
|
||||||
title: computeStateName(stateObj),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around if entity has accuracy
|
|
||||||
if (gpsAccuracy) {
|
|
||||||
mapItems.push(
|
|
||||||
Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
|
||||||
color: this._getColor(entityId),
|
|
||||||
radius: gpsAccuracy,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
|
||||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
|
||||||
this._mapPaths.forEach((marker) => map.addLayer(marker));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _attachObserver(): Promise<void> {
|
|
||||||
// Observe changes to map size and invalidate to prevent broken rendering
|
|
||||||
|
|
||||||
if (!this._resizeObserver) {
|
|
||||||
await installResizeObserver();
|
|
||||||
this._resizeObserver = new ResizeObserver(this._debouncedResizeListener);
|
|
||||||
}
|
|
||||||
this._resizeObserver.observe(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getHistory(): Promise<void> {
|
private async _getHistory(): Promise<void> {
|
||||||
this._date = new Date();
|
this._date = new Date();
|
||||||
@@ -601,9 +312,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityIds = this._configEntities!.map((entity) => entity.entity).join(
|
const entityIds = this._configEntities!.join(",");
|
||||||
","
|
|
||||||
);
|
|
||||||
const endTime = new Date();
|
const endTime = new Date();
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
|
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
|
||||||
@@ -624,7 +333,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
if (stateHistory.length < 1) {
|
if (stateHistory.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._history = stateHistory;
|
this._history = stateHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,13 +344,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
this._history = undefined;
|
this._history = undefined;
|
||||||
} else {
|
} else {
|
||||||
// remove unused entities
|
// remove unused entities
|
||||||
const configEntityIds = this._configEntities?.map(
|
|
||||||
(configEntity) => configEntity.entity
|
|
||||||
);
|
|
||||||
this._history = this._history!.reduce(
|
this._history = this._history!.reduce(
|
||||||
(accumulator: HassEntity[][], entityStates) => {
|
(accumulator: HassEntity[][], entityStates) => {
|
||||||
const entityId = entityStates[0].entity_id;
|
const entityId = entityStates[0].entity_id;
|
||||||
if (configEntityIds?.includes(entityId)) {
|
if (this._configEntities?.includes(entityId)) {
|
||||||
accumulator.push(entityStates);
|
accumulator.push(entityStates);
|
||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
@@ -660,7 +365,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#map {
|
ha-map {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
border: none;
|
border: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -671,7 +376,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button {
|
mwc-icon-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 75px;
|
top: 75px;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
@@ -685,14 +390,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
:host([ispanel]) #root {
|
:host([ispanel]) #root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light {
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,18 +33,18 @@ export class HuiActionEditor extends LitElement {
|
|||||||
@property() protected hass?: HomeAssistant;
|
@property() protected hass?: HomeAssistant;
|
||||||
|
|
||||||
get _navigation_path(): string {
|
get _navigation_path(): string {
|
||||||
const config = this.config as NavigateActionConfig;
|
const config = this.config as NavigateActionConfig | undefined;
|
||||||
return config.navigation_path || "";
|
return config?.navigation_path || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get _url_path(): string {
|
get _url_path(): string {
|
||||||
const config = this.config as UrlActionConfig;
|
const config = this.config as UrlActionConfig | undefined;
|
||||||
return config.url_path || "";
|
return config?.url_path || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get _service(): string {
|
get _service(): string {
|
||||||
const config = this.config as CallServiceActionConfig;
|
const config = this.config as CallServiceActionConfig;
|
||||||
return config.service || "";
|
return config?.service || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serviceAction = memoizeOne(
|
private _serviceAction = memoizeOne(
|
||||||
|
@@ -29,6 +29,7 @@ export class HuiInputListEditor extends LitElement {
|
|||||||
.index=${index}
|
.index=${index}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
@blur=${this._consolidateEntries}
|
@blur=${this._consolidateEntries}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
><ha-icon-button
|
><ha-icon-button
|
||||||
slot="suffix"
|
slot="suffix"
|
||||||
class="clear-button"
|
class="clear-button"
|
||||||
@@ -70,6 +71,13 @@ export class HuiInputListEditor extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleKeyDown(ev: KeyboardEvent) {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._consolidateEntries(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _consolidateEntries(ev: Event): void {
|
private _consolidateEntries(ev: Event): void {
|
||||||
const target = ev.target! as EditorTarget;
|
const target = ev.target! as EditorTarget;
|
||||||
if (target.value === "") {
|
if (target.value === "") {
|
||||||
|
@@ -34,7 +34,7 @@ const cardConfigStruct = object({
|
|||||||
dark_mode: optional(boolean()),
|
dark_mode: optional(boolean()),
|
||||||
entities: array(entitiesConfigStruct),
|
entities: array(entitiesConfigStruct),
|
||||||
hours_to_show: optional(number()),
|
hours_to_show: optional(number()),
|
||||||
geo_location_sources: optional(array()),
|
geo_location_sources: optional(array(string())),
|
||||||
});
|
});
|
||||||
|
|
||||||
@customElement("hui-map-card-editor")
|
@customElement("hui-map-card-editor")
|
||||||
|
@@ -1,88 +0,0 @@
|
|||||||
import "@polymer/iron-image/iron-image";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { EventsMixin } from "../../mixins/events-mixin";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HaEntityMarker extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="iron-positioning"></style>
|
|
||||||
<style>
|
|
||||||
.marker {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 2.5em;
|
|
||||||
text-align: center;
|
|
||||||
height: 2.5em;
|
|
||||||
line-height: 2.5em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0.1em solid var(--ha-marker-color, var(--primary-color));
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
background-color: var(--card-background-color);
|
|
||||||
}
|
|
||||||
iron-image {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="marker" style$="border-color:{{entityColor}}">
|
|
||||||
<template is="dom-if" if="[[entityName]]">[[entityName]]</template>
|
|
||||||
<template is="dom-if" if="[[entityPicture]]">
|
|
||||||
<iron-image
|
|
||||||
sizing="cover"
|
|
||||||
class="fit"
|
|
||||||
src="[[entityPicture]]"
|
|
||||||
></iron-image>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
entityId: {
|
|
||||||
type: String,
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
entityName: {
|
|
||||||
type: String,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
entityPicture: {
|
|
||||||
type: String,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
entityColor: {
|
|
||||||
type: String,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener("click", (ev) => this.badgeTap(ev));
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeTap(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (this.entityId) {
|
|
||||||
this.fire("hass-more-info", { entityId: this.entityId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-entity-marker", HaEntityMarker);
|
|
@@ -1,263 +0,0 @@
|
|||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import {
|
|
||||||
replaceTileLayer,
|
|
||||||
setupLeafletMap,
|
|
||||||
} from "../../common/dom/setup-leaflet-map";
|
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
|
||||||
import { navigate } from "../../common/navigate";
|
|
||||||
import "../../components/ha-icon";
|
|
||||||
import "../../components/ha-menu-button";
|
|
||||||
import { defaultRadiusColor } from "../../data/zone";
|
|
||||||
import "../../layouts/ha-app-layout";
|
|
||||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
|
||||||
import "../../styles/polymer-ha-style";
|
|
||||||
import "./ha-entity-marker";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaPanelMap extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
#map {
|
|
||||||
height: calc(100vh - var(--header-height));
|
|
||||||
width: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-app-layout>
|
|
||||||
<app-header fixed slot="header">
|
|
||||||
<app-toolbar>
|
|
||||||
<ha-menu-button
|
|
||||||
hass="[[hass]]"
|
|
||||||
narrow="[[narrow]]"
|
|
||||||
></ha-menu-button>
|
|
||||||
<div main-title>[[localize('panel.map')]]</div>
|
|
||||||
<template is="dom-if" if="[[computeShowEditZone(hass)]]">
|
|
||||||
<ha-icon-button
|
|
||||||
icon="hass:pencil"
|
|
||||||
on-click="openZonesEditor"
|
|
||||||
></ha-icon-button>
|
|
||||||
</template>
|
|
||||||
</app-toolbar>
|
|
||||||
</app-header>
|
|
||||||
<div id="map"></div>
|
|
||||||
</ha-app-layout>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: "drawEntities",
|
|
||||||
},
|
|
||||||
narrow: Boolean,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.loadMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMap() {
|
|
||||||
this._darkMode = this.hass.themes.darkMode;
|
|
||||||
[this._map, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
|
||||||
this.$.map,
|
|
||||||
this._darkMode
|
|
||||||
);
|
|
||||||
this.drawEntities(this.hass);
|
|
||||||
this._map.invalidateSize();
|
|
||||||
this.fitMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (this._map) {
|
|
||||||
this._map.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeShowEditZone(hass) {
|
|
||||||
return !__DEMO__ && hass.user.is_admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
openZonesEditor() {
|
|
||||||
navigate("/config/zone");
|
|
||||||
}
|
|
||||||
|
|
||||||
fitMap() {
|
|
||||||
let bounds;
|
|
||||||
|
|
||||||
if (this._mapItems.length === 0) {
|
|
||||||
this._map.setView(
|
|
||||||
new this.Leaflet.LatLng(
|
|
||||||
this.hass.config.latitude,
|
|
||||||
this.hass.config.longitude
|
|
||||||
),
|
|
||||||
14
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bounds = new this.Leaflet.latLngBounds(
|
|
||||||
this._mapItems.map((item) => item.getLatLng())
|
|
||||||
);
|
|
||||||
this._map.fitBounds(bounds.pad(0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawEntities(hass) {
|
|
||||||
/* eslint-disable vars-on-top */
|
|
||||||
const map = this._map;
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
if (this._darkMode !== this.hass.themes.darkMode) {
|
|
||||||
this._darkMode = this.hass.themes.darkMode;
|
|
||||||
this._tileLayer = replaceTileLayer(
|
|
||||||
this.Leaflet,
|
|
||||||
map,
|
|
||||||
this._tileLayer,
|
|
||||||
this.hass.themes.darkMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._mapItems) {
|
|
||||||
this._mapItems.forEach(function (marker) {
|
|
||||||
marker.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const mapItems = (this._mapItems = []);
|
|
||||||
|
|
||||||
if (this._mapZones) {
|
|
||||||
this._mapZones.forEach(function (marker) {
|
|
||||||
marker.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const mapZones = (this._mapZones = []);
|
|
||||||
|
|
||||||
Object.keys(hass.states).forEach((entityId) => {
|
|
||||||
const entity = hass.states[entityId];
|
|
||||||
|
|
||||||
if (
|
|
||||||
entity.state === "home" ||
|
|
||||||
!("latitude" in entity.attributes) ||
|
|
||||||
!("longitude" in entity.attributes)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = computeStateName(entity);
|
|
||||||
let icon;
|
|
||||||
|
|
||||||
if (computeStateDomain(entity) === "zone") {
|
|
||||||
// DRAW ZONE
|
|
||||||
if (entity.attributes.passive) return;
|
|
||||||
|
|
||||||
// create icon
|
|
||||||
let iconHTML = "";
|
|
||||||
if (entity.attributes.icon) {
|
|
||||||
const el = document.createElement("ha-icon");
|
|
||||||
el.setAttribute("icon", entity.attributes.icon);
|
|
||||||
iconHTML = el.outerHTML;
|
|
||||||
} else {
|
|
||||||
const el = document.createElement("span");
|
|
||||||
el.innerHTML = title;
|
|
||||||
iconHTML = el.outerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
icon = this.Leaflet.divIcon({
|
|
||||||
html: iconHTML,
|
|
||||||
iconSize: [24, 24],
|
|
||||||
className: "icon",
|
|
||||||
});
|
|
||||||
|
|
||||||
// create marker with the icon
|
|
||||||
mapZones.push(
|
|
||||||
this.Leaflet.marker(
|
|
||||||
[entity.attributes.latitude, entity.attributes.longitude],
|
|
||||||
{
|
|
||||||
icon: icon,
|
|
||||||
interactive: false,
|
|
||||||
title: title,
|
|
||||||
}
|
|
||||||
).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around it
|
|
||||||
mapZones.push(
|
|
||||||
this.Leaflet.circle(
|
|
||||||
[entity.attributes.latitude, entity.attributes.longitude],
|
|
||||||
{
|
|
||||||
interactive: false,
|
|
||||||
color: defaultRadiusColor,
|
|
||||||
radius: entity.attributes.radius,
|
|
||||||
}
|
|
||||||
).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAW ENTITY
|
|
||||||
// create icon
|
|
||||||
const entityPicture = entity.attributes.entity_picture || "";
|
|
||||||
const entityName = title
|
|
||||||
.split(" ")
|
|
||||||
.map(function (part) {
|
|
||||||
return part.substr(0, 1);
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
/* 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. */
|
|
||||||
icon = this.Leaflet.divIcon({
|
|
||||||
html:
|
|
||||||
"<ha-entity-marker entity-id='" +
|
|
||||||
entity.entity_id +
|
|
||||||
"' entity-name='" +
|
|
||||||
entityName +
|
|
||||||
"' entity-picture='" +
|
|
||||||
entityPicture +
|
|
||||||
"'></ha-entity-marker>",
|
|
||||||
iconSize: [45, 45],
|
|
||||||
className: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// create market with the icon
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.marker(
|
|
||||||
[entity.attributes.latitude, entity.attributes.longitude],
|
|
||||||
{
|
|
||||||
icon: icon,
|
|
||||||
title: computeStateName(entity),
|
|
||||||
}
|
|
||||||
).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around if entity has accuracy
|
|
||||||
if (entity.attributes.gps_accuracy) {
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.circle(
|
|
||||||
[entity.attributes.latitude, entity.attributes.longitude],
|
|
||||||
{
|
|
||||||
interactive: false,
|
|
||||||
color: "#0288D1",
|
|
||||||
radius: entity.attributes.gps_accuracy,
|
|
||||||
}
|
|
||||||
).addTo(map)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-panel-map", HaPanelMap);
|
|
103
src/panels/map/ha-panel-map.ts
Normal file
103
src/panels/map/ha-panel-map.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { mdiPencil } from "@mdi/js";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||||
|
import { property } from "lit/decorators";
|
||||||
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
|
import { navigate } from "../../common/navigate";
|
||||||
|
import "../../components/ha-svg-icon";
|
||||||
|
import "../../components/ha-menu-button";
|
||||||
|
import "../../layouts/ha-app-layout";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../../components/map/ha-map";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
|
||||||
|
class HaPanelMap extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
private _entities: string[] = [];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-app-layout>
|
||||||
|
<app-header fixed slot="header">
|
||||||
|
<app-toolbar>
|
||||||
|
<ha-menu-button
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
></ha-menu-button>
|
||||||
|
<div main-title>${this.hass.localize("panel.map")}</div>
|
||||||
|
${!__DEMO__ && this.hass.user?.is_admin
|
||||||
|
? html`<mwc-icon-button @click=${this._openZonesEditor}
|
||||||
|
><ha-svg-icon .path=${mdiPencil}></ha-svg-icon
|
||||||
|
></mwc-icon-button>`
|
||||||
|
: ""}
|
||||||
|
</app-toolbar>
|
||||||
|
</app-header>
|
||||||
|
<ha-map .hass=${this.hass} .entities=${this._entities} autoFit></ha-map>
|
||||||
|
</ha-app-layout>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openZonesEditor() {
|
||||||
|
navigate("/config/zone");
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
if (!changedProps.has("hass")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
this._getStates(oldHass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getStates(oldHass?: HomeAssistant) {
|
||||||
|
let changed = false;
|
||||||
|
const personSources = new Set<string>();
|
||||||
|
const locationEntities: string[] = [];
|
||||||
|
Object.values(this.hass!.states).forEach((entity) => {
|
||||||
|
if (
|
||||||
|
entity.state === "home" ||
|
||||||
|
!("latitude" in entity.attributes) ||
|
||||||
|
!("longitude" in entity.attributes)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
locationEntities.push(entity.entity_id);
|
||||||
|
if (computeStateDomain(entity) === "person" && entity.attributes.source) {
|
||||||
|
personSources.add(entity.attributes.source);
|
||||||
|
}
|
||||||
|
if (oldHass?.states[entity.entity_id] !== entity) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this._entities = locationEntities.filter(
|
||||||
|
(entity) => !personSources.has(entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
ha-map {
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("ha-panel-map", HaPanelMap);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-panel-map": HaPanelMap;
|
||||||
|
}
|
||||||
|
}
|
106
src/panels/profile/ha-long-lived-access-token-dialog.ts
Normal file
106
src/panels/profile/ha-long-lived-access-token-dialog.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { createCloseHeading } from "../../components/ha-dialog";
|
||||||
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog";
|
||||||
|
|
||||||
|
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
||||||
|
|
||||||
|
@customElement("ha-long-lived-access-token-dialog")
|
||||||
|
export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: LongLivedAccessTokenDialogParams;
|
||||||
|
|
||||||
|
@state() private _qrCode?: TemplateResult;
|
||||||
|
|
||||||
|
public showDialog(params: LongLivedAccessTokenDialogParams): void {
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._params = undefined;
|
||||||
|
this._qrCode = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._params || !this._params.token) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
hideActions
|
||||||
|
.heading=${createCloseHeading(this.hass, this._params.name)}
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<paper-input
|
||||||
|
dialogInitialFocus
|
||||||
|
.value=${this._params.token}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
||||||
|
)}
|
||||||
|
type="text"
|
||||||
|
></paper-input>
|
||||||
|
<div id="qr">
|
||||||
|
${this._qrCode
|
||||||
|
? this._qrCode
|
||||||
|
: html`
|
||||||
|
<mwc-button @click=${this._generateQR}>
|
||||||
|
Generate QR code
|
||||||
|
</mwc-button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _generateQR() {
|
||||||
|
const qrcode = await import("qrcode");
|
||||||
|
const canvas = await qrcode.toCanvas(this._params?.token, {
|
||||||
|
width: 180,
|
||||||
|
errorCorrectionLevel: "Q",
|
||||||
|
});
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const imageObj = new Image();
|
||||||
|
imageObj.src = QR_LOGO_URL;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
imageObj.onload = resolve;
|
||||||
|
});
|
||||||
|
context.drawImage(
|
||||||
|
imageObj,
|
||||||
|
canvas.width / 3,
|
||||||
|
canvas.height / 3,
|
||||||
|
canvas.width / 3,
|
||||||
|
canvas.height / 3
|
||||||
|
);
|
||||||
|
|
||||||
|
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
#qr {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-long-lived-access-token-dialog": HaLongLivedAccessTokenDialog;
|
||||||
|
}
|
||||||
|
}
|
@@ -18,6 +18,7 @@ import {
|
|||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import "../../styles/polymer-ha-style";
|
import "../../styles/polymer-ha-style";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { showLongLivedAccessTokenDialog } from "./show-long-lived-access-token-dialog";
|
||||||
|
|
||||||
@customElement("ha-long-lived-access-tokens-card")
|
@customElement("ha-long-lived-access-tokens-card")
|
||||||
class HaLongLivedTokens extends LitElement {
|
class HaLongLivedTokens extends LitElement {
|
||||||
@@ -118,13 +119,7 @@ class HaLongLivedTokens extends LitElement {
|
|||||||
client_name: name,
|
client_name: name,
|
||||||
});
|
});
|
||||||
|
|
||||||
showPromptDialog(this, {
|
showLongLivedAccessTokenDialog(this, { token, name });
|
||||||
title: name,
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
|
||||||
),
|
|
||||||
defaultValue: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent(this, "hass-refresh-tokens");
|
fireEvent(this, "hass-refresh-tokens");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@@ -36,7 +36,9 @@ export class HaPickThemeRow extends LitElement {
|
|||||||
const hasThemes =
|
const hasThemes =
|
||||||
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
||||||
const curTheme =
|
const curTheme =
|
||||||
this.hass.selectedTheme?.theme || this.hass.themes.default_theme;
|
this.hass.selectedTheme?.theme || this.hass.themes.darkMode
|
||||||
|
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||||
|
: this.hass.themes.default_theme;
|
||||||
|
|
||||||
const themeSettings = this.hass.selectedTheme;
|
const themeSettings = this.hass.selectedTheme;
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||||
import "@polymer/iron-label/iron-label";
|
|
||||||
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";
|
||||||
|
17
src/panels/profile/show-long-lived-access-token-dialog.ts
Normal file
17
src/panels/profile/show-long-lived-access-token-dialog.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface LongLivedAccessTokenDialogParams {
|
||||||
|
token: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showLongLivedAccessTokenDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
longLivedAccessTokenDialogParams: LongLivedAccessTokenDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-long-lived-access-token-dialog",
|
||||||
|
dialogImport: () => import("./ha-long-lived-access-token-dialog"),
|
||||||
|
dialogParams: longLivedAccessTokenDialogParams,
|
||||||
|
});
|
||||||
|
};
|
@@ -1,10 +1,10 @@
|
|||||||
// For localize
|
// For localize
|
||||||
import "@formatjs/intl-getcanonicallocales/polyfill";
|
|
||||||
import "lit/polyfill-support";
|
|
||||||
import "core-js";
|
import "core-js";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
import "lit/polyfill-support";
|
||||||
|
import "@formatjs/intl-getcanonicallocales/polyfill";
|
||||||
// To use comlink under ES5
|
// To use comlink under ES5
|
||||||
import "proxy-polyfill";
|
import "proxy-polyfill";
|
||||||
import "regenerator-runtime/runtime";
|
|
||||||
import "unfetch/polyfill";
|
import "unfetch/polyfill";
|
||||||
|
|
||||||
// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
|
// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
|
||||||
@@ -32,3 +32,16 @@ import "unfetch/polyfill";
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
|
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
|
||||||
|
|
||||||
|
// Source: https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNames
|
||||||
|
if (Element.prototype.getAttributeNames === undefined) {
|
||||||
|
Element.prototype.getAttributeNames = function () {
|
||||||
|
const attributes = this.attributes;
|
||||||
|
const length = attributes.length;
|
||||||
|
const result = new Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result[i] = attributes[i].name;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -29,10 +29,10 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--disabled-text-color: #bdbdbd;
|
--disabled-text-color: #bdbdbd;
|
||||||
|
|
||||||
/* main interface colors */
|
/* main interface colors */
|
||||||
--primary-color: #03a9f4;
|
--primary-color: ${DEFAULT_PRIMARY_COLOR};
|
||||||
--dark-primary-color: #0288d1;
|
--dark-primary-color: #0288d1;
|
||||||
--light-primary-color: #b3e5fC;
|
--light-primary-color: #b3e5fC;
|
||||||
--accent-color: #ff9800;
|
--accent-color: ${DEFAULT_ACCENT_COLOR};
|
||||||
--divider-color: rgba(0, 0, 0, .12);
|
--divider-color: rgba(0, 0, 0, .12);
|
||||||
|
|
||||||
--scrollbar-thumb-color: rgb(194, 194, 194);
|
--scrollbar-thumb-color: rgb(194, 194, 194);
|
||||||
|
39
src/resources/lit-virtualizer/lib/lit-virtualizer.d.ts
vendored
Normal file
39
src/resources/lit-virtualizer/lib/lit-virtualizer.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { LitElement, TemplateResult } from "lit";
|
||||||
|
import {
|
||||||
|
LayoutSpecifier,
|
||||||
|
Layout,
|
||||||
|
LayoutConstructor,
|
||||||
|
} from "./uni-virtualizer/lib/layouts/Layout.js";
|
||||||
|
/**
|
||||||
|
* A LitElement wrapper of the scroll directive.
|
||||||
|
*
|
||||||
|
* Import this module to declare the lit-virtualizer custom element.
|
||||||
|
* Pass an items array, renderItem method, and scroll target as properties
|
||||||
|
* to the <lit-virtualizer> element.
|
||||||
|
*/
|
||||||
|
export declare class LitVirtualizer extends LitElement {
|
||||||
|
renderItem?: (item: any, index?: number) => TemplateResult;
|
||||||
|
items: Array<unknown>;
|
||||||
|
scrollTarget: Element | Window;
|
||||||
|
keyFunction: ((item: unknown) => unknown) | undefined;
|
||||||
|
private _layout;
|
||||||
|
private _scrollToIndex;
|
||||||
|
createRenderRoot(): this;
|
||||||
|
/**
|
||||||
|
* The method used for rendering each item.
|
||||||
|
*/
|
||||||
|
set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null);
|
||||||
|
get layout(): Layout | LayoutConstructor | LayoutSpecifier | null;
|
||||||
|
/**
|
||||||
|
* Scroll to the specified index, placing that item at the given position
|
||||||
|
* in the scroll view.
|
||||||
|
*/
|
||||||
|
scrollToIndex(index: number, position?: string): Promise<void>;
|
||||||
|
render(): TemplateResult;
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"lit-virtualizer": LitVirtualizer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=lit-virtualizer.d.ts.map
|
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"lit-virtualizer.d.ts","sourceRoot":"","sources":["../src/lib/lit-virtualizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAKvD,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAErG;;;;;;GAMG;AACH,qBACa,cAAe,SAAQ,UAAU;IAE1C,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC;IAG7D,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAM;IAG3B,YAAY,EAAE,OAAO,GAAG,MAAM,CAAQ;IAGtC,WAAW,EAAE,CAAC,CAAC,IAAI,EAAC,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS,CAAa;IAEjE,OAAO,CAAC,OAAO,CAA6D;IAE5E,OAAO,CAAC,cAAc,CAAkD;IAExE,gBAAgB;IAahB;;OAEG;IAWH,IACI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,EAIrE;IAED,IAAI,MAAM,IAAI,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAEhE;IAGD;;;OAGG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAgB;IAO7D,MAAM,IAAI,cAAc;CAO3B;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAE,cAAc,CAAC;KACrC;CACJ"}
|
118
src/resources/lit-virtualizer/lib/lit-virtualizer.js
Normal file
118
src/resources/lit-virtualizer/lib/lit-virtualizer.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
var __decorate =
|
||||||
|
(this && this.__decorate) ||
|
||||||
|
function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length,
|
||||||
|
r =
|
||||||
|
c < 3
|
||||||
|
? target
|
||||||
|
: desc === null
|
||||||
|
? (desc = Object.getOwnPropertyDescriptor(target, key))
|
||||||
|
: desc,
|
||||||
|
d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
||||||
|
r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else
|
||||||
|
for (var i = decorators.length - 1; i >= 0; i--)
|
||||||
|
if ((d = decorators[i]))
|
||||||
|
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators/custom-element.js";
|
||||||
|
import { property } from "lit/decorators/property.js";
|
||||||
|
import { scroll } from "./scroll.js";
|
||||||
|
import { scrollerRef } from "./uni-virtualizer/lib/VirtualScroller.js";
|
||||||
|
/**
|
||||||
|
* A LitElement wrapper of the scroll directive.
|
||||||
|
*
|
||||||
|
* Import this module to declare the lit-virtualizer custom element.
|
||||||
|
* Pass an items array, renderItem method, and scroll target as properties
|
||||||
|
* to the <lit-virtualizer> element.
|
||||||
|
*/
|
||||||
|
let LitVirtualizer = class LitVirtualizer extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.items = [];
|
||||||
|
this.scrollTarget = this;
|
||||||
|
this.keyFunction = undefined;
|
||||||
|
this._layout = null;
|
||||||
|
this._scrollToIndex = null;
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
// get items() {
|
||||||
|
// return this._items;
|
||||||
|
// }
|
||||||
|
// set items(items) {
|
||||||
|
// this._items = items;
|
||||||
|
// this._scroller.totalItems = items.length;
|
||||||
|
// }
|
||||||
|
/**
|
||||||
|
* The method used for rendering each item.
|
||||||
|
*/
|
||||||
|
// get renderItem() {
|
||||||
|
// return this._renderItem;
|
||||||
|
// }
|
||||||
|
// set renderItem(renderItem) {
|
||||||
|
// if (renderItem !== this.renderItem) {
|
||||||
|
// this._renderItem = renderItem;
|
||||||
|
// this.requestUpdate();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
set layout(layout) {
|
||||||
|
// TODO (graynorton): Shouldn't have to set this here
|
||||||
|
this._layout = layout;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
get layout() {
|
||||||
|
return this[scrollerRef].layout;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Scroll to the specified index, placing that item at the given position
|
||||||
|
* in the scroll view.
|
||||||
|
*/
|
||||||
|
async scrollToIndex(index, position = "start") {
|
||||||
|
this._scrollToIndex = { index, position };
|
||||||
|
this.requestUpdate();
|
||||||
|
await this.updateComplete;
|
||||||
|
this._scrollToIndex = null;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { items, renderItem, keyFunction, scrollTarget } = this;
|
||||||
|
const layout = this._layout;
|
||||||
|
return html`
|
||||||
|
${scroll({
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
layout,
|
||||||
|
keyFunction,
|
||||||
|
scrollTarget,
|
||||||
|
scrollToIndex: this._scrollToIndex,
|
||||||
|
})}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorate([property()], LitVirtualizer.prototype, "renderItem", void 0);
|
||||||
|
__decorate(
|
||||||
|
[property({ attribute: false })],
|
||||||
|
LitVirtualizer.prototype,
|
||||||
|
"items",
|
||||||
|
void 0
|
||||||
|
);
|
||||||
|
__decorate(
|
||||||
|
[property({ attribute: false })],
|
||||||
|
LitVirtualizer.prototype,
|
||||||
|
"scrollTarget",
|
||||||
|
void 0
|
||||||
|
);
|
||||||
|
__decorate([property()], LitVirtualizer.prototype, "keyFunction", void 0);
|
||||||
|
__decorate(
|
||||||
|
[property({ attribute: false })],
|
||||||
|
LitVirtualizer.prototype,
|
||||||
|
"layout",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
LitVirtualizer = __decorate([customElement("lit-virtualizer")], LitVirtualizer);
|
||||||
|
export { LitVirtualizer };
|
||||||
|
//# sourceMappingURL=lit-virtualizer.js.map
|
1
src/resources/lit-virtualizer/lib/lit-virtualizer.js.map
Normal file
1
src/resources/lit-virtualizer/lib/lit-virtualizer.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"lit-virtualizer.js","sourceRoot":"","sources":["../src/lib/lit-virtualizer.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAkB,MAAM,KAAK,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAoB,MAAM,0CAA0C,CAAC;AAGzF;;;;;;GAMG;AAEH,IAAa,cAAc,GAA3B,MAAa,cAAe,SAAQ,UAAU;IAA9C;;QAKI,UAAK,GAAmB,EAAE,CAAC;QAG3B,iBAAY,GAAqB,IAAI,CAAC;QAGtC,gBAAW,GAA4C,SAAS,CAAC;QAEzD,YAAO,GAAwD,IAAI,CAAC;QAEpE,mBAAc,GAA6C,IAAI,CAAC;IA0D5E,CAAC;IAxDG,gBAAgB;QACZ,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gBAAgB;IAChB,0BAA0B;IAC1B,IAAI;IAEJ,qBAAqB;IACrB,2BAA2B;IAC3B,gDAAgD;IAChD,IAAI;IAEJ;;OAEG;IACH,qBAAqB;IACrB,+BAA+B;IAC/B,IAAI;IACJ,+BAA+B;IAC/B,4CAA4C;IAC5C,yCAAyC;IACzC,gCAAgC;IAChC,QAAQ;IACR,IAAI;IAGJ,IAAI,MAAM,CAAC,MAA2D;QAClE,qDAAqD;QACrD,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,aAAa,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,MAAM;QACN,OAAQ,IAAyB,CAAC,WAAW,CAAE,CAAC,MAAM,CAAC;IAC3D,CAAC;IAGD;;;OAGG;IACH,KAAK,CAAC,aAAa,CAAC,KAAa,EAAE,WAAmB,OAAO;QACzD,IAAI,CAAC,cAAc,GAAG,EAAC,KAAK,EAAE,QAAQ,EAAC,CAAC;QACxC,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,MAAM,IAAI,CAAC,cAAc,CAAC;QAC1B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;IAC/B,CAAC;IAED,MAAM;QACF,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;QAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,OAAO,IAAI,CAAA;cACL,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;SACzG,CAAC;IACN,CAAC;CACJ,CAAA;AAvEG;IADC,QAAQ,EAAE;kDACkD;AAG7D;IADC,QAAQ,CAAC,EAAC,SAAS,EAAE,KAAK,EAAC,CAAC;6CACF;AAG3B;IADC,QAAQ,CAAC,EAAC,SAAS,EAAE,KAAK,EAAC,CAAC;oDACS;AAGtC;IADC,QAAQ,EAAE;mDACsD;AAiCjE;IADC,QAAQ,CAAC,EAAC,SAAS,EAAC,KAAK,EAAC,CAAC;4CAK3B;AAhDQ,cAAc;IAD1B,aAAa,CAAC,iBAAiB,CAAC;GACpB,cAAc,CAyE1B;SAzEY,cAAc","sourcesContent":["import { html, LitElement, TemplateResult } from 'lit';\nimport { customElement } from 'lit/decorators/custom-element.js';\nimport { property } from 'lit/decorators/property.js';\nimport { scroll } from './scroll.js';\nimport { scrollerRef, ContainerElement } from './uni-virtualizer/lib/VirtualScroller.js';\nimport { LayoutSpecifier, Layout, LayoutConstructor } from './uni-virtualizer/lib/layouts/Layout.js';\n\n/**\n * A LitElement wrapper of the scroll directive.\n *\n * Import this module to declare the lit-virtualizer custom element.\n * Pass an items array, renderItem method, and scroll target as properties\n * to the <lit-virtualizer> element.\n */\n@customElement('lit-virtualizer')\nexport class LitVirtualizer extends LitElement {\n @property()\n renderItem?: ((item: any, index?: number) => TemplateResult);\n\n @property({attribute: false})\n items: Array<unknown> = [];\n\n @property({attribute: false})\n scrollTarget: Element | Window = this;\n\n @property()\n keyFunction: ((item:unknown) => unknown) | undefined = undefined;\n\n private _layout: Layout | LayoutConstructor | LayoutSpecifier | null = null;\n\n private _scrollToIndex: {index: number, position: string} | null = null;\n \n createRenderRoot() {\n return this;\n }\n\n // get items() {\n // return this._items;\n // }\n\n // set items(items) {\n // this._items = items;\n // this._scroller.totalItems = items.length;\n // }\n\n /**\n * The method used for rendering each item.\n */\n // get renderItem() {\n // return this._renderItem;\n // }\n // set renderItem(renderItem) {\n // if (renderItem !== this.renderItem) {\n // this._renderItem = renderItem;\n // this.requestUpdate();\n // }\n // }\n\n @property({attribute:false})\n set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null) {\n // TODO (graynorton): Shouldn't have to set this here\n this._layout = layout;\n this.requestUpdate();\n }\n\n get layout(): Layout | LayoutConstructor | LayoutSpecifier | null {\n return (this as ContainerElement)[scrollerRef]!.layout;\n }\n \n \n /**\n * Scroll to the specified index, placing that item at the given position\n * in the scroll view.\n */\n async scrollToIndex(index: number, position: string = 'start') {\n this._scrollToIndex = {index, position};\n this.requestUpdate();\n await this.updateComplete;\n this._scrollToIndex = null;\n }\n\n render(): TemplateResult {\n const { items, renderItem, keyFunction, scrollTarget } = this;\n const layout = this._layout;\n return html`\n ${scroll({ items, renderItem, layout, keyFunction, scrollTarget, scrollToIndex: this._scrollToIndex })}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'lit-virtualizer': LitVirtualizer;\n }\n}"]}
|
61
src/resources/lit-virtualizer/lib/scroll.d.ts
vendored
Normal file
61
src/resources/lit-virtualizer/lib/scroll.d.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { TemplateResult, ChildPart } from "lit";
|
||||||
|
import { PartInfo } from "lit/directive.js";
|
||||||
|
import { AsyncDirective } from "lit/async-directive.js";
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
LayoutConstructor,
|
||||||
|
LayoutSpecifier,
|
||||||
|
} from "./uni-virtualizer/lib/layouts/Layout.js";
|
||||||
|
import {
|
||||||
|
VirtualScroller,
|
||||||
|
ScrollToIndexValue,
|
||||||
|
} from "./uni-virtualizer/lib/VirtualScroller.js";
|
||||||
|
/**
|
||||||
|
* Configuration options for the scroll directive.
|
||||||
|
*/
|
||||||
|
interface ScrollConfig {
|
||||||
|
/**
|
||||||
|
* A function that returns a lit-html TemplateResult. It will be used
|
||||||
|
* to generate the DOM for each item in the virtual list.
|
||||||
|
*/
|
||||||
|
renderItem?: (item: any, index?: number) => TemplateResult;
|
||||||
|
keyFunction?: (item: any) => unknown;
|
||||||
|
layout?: Layout | LayoutConstructor | LayoutSpecifier | null;
|
||||||
|
/**
|
||||||
|
* An element that receives scroll events for the virtual scroller.
|
||||||
|
*/
|
||||||
|
scrollTarget?: Element | Window;
|
||||||
|
/**
|
||||||
|
* The list of items to display via the renderItem function.
|
||||||
|
*/
|
||||||
|
items?: Array<any>;
|
||||||
|
/**
|
||||||
|
* Limit for the number of items to display. Defaults to the length of the
|
||||||
|
* items array.
|
||||||
|
*/
|
||||||
|
totalItems?: number;
|
||||||
|
/**
|
||||||
|
* Index and position of the item to scroll to.
|
||||||
|
*/
|
||||||
|
scrollToIndex?: ScrollToIndexValue;
|
||||||
|
}
|
||||||
|
export declare const defaultKeyFunction: (item: any) => any;
|
||||||
|
export declare const defaultRenderItem: (item: any) => TemplateResult<1>;
|
||||||
|
declare class ScrollDirective extends AsyncDirective {
|
||||||
|
container: HTMLElement | null;
|
||||||
|
scroller: VirtualScroller | null;
|
||||||
|
first: number;
|
||||||
|
last: number;
|
||||||
|
renderItem: (item: any, index?: number) => TemplateResult;
|
||||||
|
keyFunction: (item: any) => unknown;
|
||||||
|
items: Array<unknown>;
|
||||||
|
constructor(part: PartInfo);
|
||||||
|
render(config?: ScrollConfig): unknown;
|
||||||
|
update(part: ChildPart, [config]: [ScrollConfig]): unknown;
|
||||||
|
private _initialize;
|
||||||
|
}
|
||||||
|
export declare const scroll: (
|
||||||
|
config?: ScrollConfig | undefined
|
||||||
|
) => import("lit-html/directive").DirectiveResult<typeof ScrollDirective>;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=scroll.d.ts.map
|
1
src/resources/lit-virtualizer/lib/scroll.d.ts.map
Normal file
1
src/resources/lit-virtualizer/lib/scroll.d.ts.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../src/lib/scroll.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAW,SAAS,EAAQ,MAAM,KAAK,CAAC;AAC/D,OAAO,EAAa,QAAQ,EAAY,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAE/F;;GAEG;AACH,UAAU,YAAY;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC;IAE3D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC;IAGrC,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAAC;IAE7D;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAEH,eAAO,MAAM,kBAAkB,SAAU,GAAG,QAAS,CAAC;AACtD,eAAO,MAAM,iBAAiB,SAAU,GAAG,sBAA2C,CAAC;AAEvF,cAAM,eAAgB,SAAQ,cAAc;IACxC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAO;IACpC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAO;IACvC,KAAK,SAAI;IACT,IAAI,SAAK;IACT,UAAU,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAqB;IAC9E,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAsB;IACzD,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAK;gBAEd,IAAI,EAAE,QAAQ;IAO1B,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY;IAc5B,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC;IAehD,OAAO,CAAC,WAAW;CAiBtB;AAED,eAAO,MAAM,MAAM,6GAA6B,CAAC"}
|
79
src/resources/lit-virtualizer/lib/scroll.js
Normal file
79
src/resources/lit-virtualizer/lib/scroll.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { nothing, html } from "lit";
|
||||||
|
import { directive, PartType } from "lit/directive.js";
|
||||||
|
import { AsyncDirective } from "lit/async-directive.js";
|
||||||
|
import { repeat } from "lit/directives/repeat.js";
|
||||||
|
import { VirtualScroller } from "./uni-virtualizer/lib/VirtualScroller.js";
|
||||||
|
export const defaultKeyFunction = (item) => item;
|
||||||
|
export const defaultRenderItem = (item) =>
|
||||||
|
html`${JSON.stringify(item, null, 2)}`;
|
||||||
|
class ScrollDirective extends AsyncDirective {
|
||||||
|
constructor(part) {
|
||||||
|
super(part);
|
||||||
|
this.container = null;
|
||||||
|
this.scroller = null;
|
||||||
|
this.first = 0;
|
||||||
|
this.last = -1;
|
||||||
|
this.renderItem = defaultRenderItem;
|
||||||
|
this.keyFunction = defaultKeyFunction;
|
||||||
|
this.items = [];
|
||||||
|
if (part.type !== PartType.CHILD) {
|
||||||
|
throw new Error(
|
||||||
|
"The scroll directive can only be used in child expressions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render(config) {
|
||||||
|
if (config) {
|
||||||
|
this.renderItem = config.renderItem || this.renderItem;
|
||||||
|
this.keyFunction = config.keyFunction || this.keyFunction;
|
||||||
|
}
|
||||||
|
const itemsToRender = [];
|
||||||
|
if (this.first >= 0 && this.last >= this.first) {
|
||||||
|
for (let i = this.first; i < this.last + 1; i++) {
|
||||||
|
itemsToRender.push(this.items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repeat(
|
||||||
|
itemsToRender,
|
||||||
|
this.keyFunction || defaultKeyFunction,
|
||||||
|
this.renderItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
update(part, [config]) {
|
||||||
|
var _a;
|
||||||
|
if (this.scroller || this._initialize(part, config)) {
|
||||||
|
const { scroller } = this;
|
||||||
|
this.items = scroller.items = config.items || [];
|
||||||
|
scroller.totalItems =
|
||||||
|
config.totalItems ||
|
||||||
|
((_a = config.items) === null || _a === void 0 ? void 0 : _a.length) ||
|
||||||
|
0;
|
||||||
|
scroller.layout = config.layout || null;
|
||||||
|
scroller.scrollTarget = config.scrollTarget || this.container;
|
||||||
|
if (config.scrollToIndex) {
|
||||||
|
scroller.scrollToIndex = config.scrollToIndex;
|
||||||
|
}
|
||||||
|
return this.render(config);
|
||||||
|
}
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
_initialize(part, config) {
|
||||||
|
const container = (this.container = part.parentNode);
|
||||||
|
if (container && container.nodeType === 1) {
|
||||||
|
this.scroller = new VirtualScroller({ container });
|
||||||
|
container.addEventListener("rangeChanged", (e) => {
|
||||||
|
this.first = e.detail.first;
|
||||||
|
this.last = e.detail.last;
|
||||||
|
this.setValue(this.render());
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// TODO (GN): This seems to be needed in the case where the `scroll`
|
||||||
|
// directive is used within the `LitVirtualizer` element. Figure out why
|
||||||
|
// and see if there's a cleaner solution.
|
||||||
|
Promise.resolve().then(() => this.update(part, [config]));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const scroll = directive(ScrollDirective);
|
||||||
|
//# sourceMappingURL=scroll.js.map
|
1
src/resources/lit-virtualizer/lib/scroll.js.map
Normal file
1
src/resources/lit-virtualizer/lib/scroll.js.map
Normal file
File diff suppressed because one or more lines are too long
230
src/resources/lit-virtualizer/lib/uni-virtualizer/lib/VirtualScroller.d.ts
vendored
Normal file
230
src/resources/lit-virtualizer/lib/uni-virtualizer/lib/VirtualScroller.d.ts
vendored
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import {
|
||||||
|
ItemBox,
|
||||||
|
Layout,
|
||||||
|
LayoutConstructor,
|
||||||
|
LayoutSpecifier,
|
||||||
|
} from "./layouts/Layout.js";
|
||||||
|
export declare const scrollerRef: unique symbol;
|
||||||
|
export declare type RangeChangeEvent = {
|
||||||
|
first: number;
|
||||||
|
last: number;
|
||||||
|
firstVisible: number;
|
||||||
|
lastVisible: number;
|
||||||
|
};
|
||||||
|
interface ElementWithOptionalScrollerRef extends Element {
|
||||||
|
[scrollerRef]?: VirtualScroller;
|
||||||
|
}
|
||||||
|
interface ShadowRootWithOptionalScrollerRef extends ShadowRoot {
|
||||||
|
[scrollerRef]?: VirtualScroller;
|
||||||
|
}
|
||||||
|
declare type Container =
|
||||||
|
| ElementWithOptionalScrollerRef
|
||||||
|
| ShadowRootWithOptionalScrollerRef;
|
||||||
|
export declare type ContainerElement = ElementWithOptionalScrollerRef;
|
||||||
|
declare type ChildMeasurements = {
|
||||||
|
[key: number]: ItemBox;
|
||||||
|
};
|
||||||
|
export declare type ScrollToIndexValue = {
|
||||||
|
index: number;
|
||||||
|
position?: string;
|
||||||
|
} | null;
|
||||||
|
export interface VirtualScrollerConfig {
|
||||||
|
layout?: Layout | LayoutConstructor | LayoutSpecifier;
|
||||||
|
/**
|
||||||
|
* An element that receives scroll events for the virtual scroller.
|
||||||
|
*/
|
||||||
|
scrollTarget?: Element | Window;
|
||||||
|
/**
|
||||||
|
* The parent of all child nodes to be rendered.
|
||||||
|
*/
|
||||||
|
container: Element | ShadowRoot;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Provides virtual scrolling boilerplate.
|
||||||
|
*
|
||||||
|
* Extensions of this class must set container, layout, and scrollTarget.
|
||||||
|
*
|
||||||
|
* Extensions of this class must also override VirtualRepeater's DOM
|
||||||
|
* manipulation methods.
|
||||||
|
*/
|
||||||
|
export declare class VirtualScroller {
|
||||||
|
private _benchmarkStart;
|
||||||
|
/**
|
||||||
|
* Whether the layout should receive an updated viewport size on the next
|
||||||
|
* render.
|
||||||
|
*/
|
||||||
|
private _layout;
|
||||||
|
/**
|
||||||
|
* The element that generates scroll events and defines the container
|
||||||
|
* viewport. Set by scrollTarget.
|
||||||
|
*/
|
||||||
|
private _scrollTarget;
|
||||||
|
/**
|
||||||
|
* A sentinel element that sizes the container when it is a scrolling
|
||||||
|
* element. This ensures the scroll bar accurately reflects the total
|
||||||
|
* size of the list.
|
||||||
|
*/
|
||||||
|
private _sizer;
|
||||||
|
/**
|
||||||
|
* Layout provides these values, we set them on _render().
|
||||||
|
* TODO @straversi: Can we find an XOR type, usable for the key here?
|
||||||
|
*/
|
||||||
|
private _scrollSize;
|
||||||
|
/**
|
||||||
|
* Difference between scroll target's current and required scroll offsets.
|
||||||
|
* Provided by layout.
|
||||||
|
*/
|
||||||
|
private _scrollErr;
|
||||||
|
/**
|
||||||
|
* A list of the positions (top, left) of the children in the current range.
|
||||||
|
*/
|
||||||
|
private _childrenPos;
|
||||||
|
private _childMeasurements;
|
||||||
|
private _toBeMeasured;
|
||||||
|
private _rangeChanged;
|
||||||
|
private _itemsChanged;
|
||||||
|
private _visibilityChanged;
|
||||||
|
/**
|
||||||
|
* Containing element. Set by container.
|
||||||
|
*/
|
||||||
|
protected _container: Container | null;
|
||||||
|
/**
|
||||||
|
* The parent of all child nodes to be rendered. Set by container.
|
||||||
|
*/
|
||||||
|
private _containerElement;
|
||||||
|
/**
|
||||||
|
* Keep track of original inline style of the container, so it can be
|
||||||
|
* restored when container is changed.
|
||||||
|
*/
|
||||||
|
private _containerInlineStyle;
|
||||||
|
/**
|
||||||
|
* Size of the container.
|
||||||
|
*/
|
||||||
|
private _containerSize;
|
||||||
|
/**
|
||||||
|
* Resize observer attached to container.
|
||||||
|
*/
|
||||||
|
private _containerRO;
|
||||||
|
/**
|
||||||
|
* Resize observer attached to children.
|
||||||
|
*/
|
||||||
|
private _childrenRO;
|
||||||
|
private _mutationObserver;
|
||||||
|
private _mutationPromise;
|
||||||
|
private _mutationPromiseResolver;
|
||||||
|
private _mutationsObserved;
|
||||||
|
private _loadListener;
|
||||||
|
/**
|
||||||
|
* Index and position of item to scroll to.
|
||||||
|
*/
|
||||||
|
private _scrollToIndex;
|
||||||
|
/**
|
||||||
|
* Items to render. Set by items.
|
||||||
|
*/
|
||||||
|
private _items;
|
||||||
|
/**
|
||||||
|
* Total number of items to render. Set by totalItems.
|
||||||
|
*/
|
||||||
|
private _totalItems;
|
||||||
|
/**
|
||||||
|
* Index of the first child in the range, not necessarily the first visible child.
|
||||||
|
* TODO @straversi: Consider renaming these.
|
||||||
|
*/
|
||||||
|
protected _first: number;
|
||||||
|
/**
|
||||||
|
* Index of the last child in the range.
|
||||||
|
*/
|
||||||
|
protected _last: number;
|
||||||
|
/**
|
||||||
|
* Index of the first item intersecting the container element.
|
||||||
|
*/
|
||||||
|
private _firstVisible;
|
||||||
|
/**
|
||||||
|
* Index of the last item intersecting the container element.
|
||||||
|
*/
|
||||||
|
private _lastVisible;
|
||||||
|
protected _scheduled: WeakSet<object>;
|
||||||
|
/**
|
||||||
|
* Invoked at the end of each render cycle: children in the range are
|
||||||
|
* measured, and their dimensions passed to this callback. Use it to layout
|
||||||
|
* children as needed.
|
||||||
|
*/
|
||||||
|
protected _measureCallback: ((sizes: ChildMeasurements) => void) | null;
|
||||||
|
protected _measureChildOverride:
|
||||||
|
| ((element: Element, item: unknown) => ItemBox)
|
||||||
|
| null;
|
||||||
|
constructor(config?: VirtualScrollerConfig);
|
||||||
|
set items(items: Array<unknown> | undefined);
|
||||||
|
/**
|
||||||
|
* The total number of items, regardless of the range, that can be rendered
|
||||||
|
* as child nodes.
|
||||||
|
*/
|
||||||
|
get totalItems(): number;
|
||||||
|
set totalItems(num: number);
|
||||||
|
/**
|
||||||
|
* The parent of all child nodes to be rendered.
|
||||||
|
*/
|
||||||
|
get container(): Container | null;
|
||||||
|
set container(container: Container | null);
|
||||||
|
get layout(): Layout | LayoutConstructor | LayoutSpecifier | null;
|
||||||
|
set layout(layout: Layout | LayoutConstructor | LayoutSpecifier | null);
|
||||||
|
startBenchmarking(): void;
|
||||||
|
stopBenchmarking(): {
|
||||||
|
timeElapsed: number;
|
||||||
|
virtualizationTime: number;
|
||||||
|
} | null;
|
||||||
|
private _measureChildren;
|
||||||
|
/**
|
||||||
|
* Returns the width, height, and margins of the given child.
|
||||||
|
*/
|
||||||
|
_measureChild(element: Element): ItemBox;
|
||||||
|
/**
|
||||||
|
* The element that generates scroll events and defines the container
|
||||||
|
* viewport. The value `null` (default) corresponds to `window` as scroll
|
||||||
|
* target.
|
||||||
|
*/
|
||||||
|
get scrollTarget(): Element | Window | null;
|
||||||
|
set scrollTarget(target: Element | Window | null);
|
||||||
|
/**
|
||||||
|
* Index and position of item to scroll to. The scroller will fix to that point
|
||||||
|
* until the user scrolls.
|
||||||
|
*/
|
||||||
|
set scrollToIndex(newValue: ScrollToIndexValue);
|
||||||
|
protected _schedule(method: Function): Promise<void>;
|
||||||
|
_updateDOM(): Promise<void>;
|
||||||
|
_updateLayout(): void;
|
||||||
|
private _handleScrollEvent;
|
||||||
|
handleEvent(event: CustomEvent): void;
|
||||||
|
private _initResizeObservers;
|
||||||
|
private _createContainerSizer;
|
||||||
|
get _children(): Array<HTMLElement>;
|
||||||
|
private _updateView;
|
||||||
|
/**
|
||||||
|
* Styles the _sizer element or the container so that its size reflects the
|
||||||
|
* total size of all items.
|
||||||
|
*/
|
||||||
|
private _sizeContainer;
|
||||||
|
/**
|
||||||
|
* Sets the top and left transform style of the children from the values in
|
||||||
|
* pos.
|
||||||
|
*/
|
||||||
|
private _positionChildren;
|
||||||
|
private _adjustRange;
|
||||||
|
private _correctScrollError;
|
||||||
|
/**
|
||||||
|
* Emits a rangechange event with the current first, last, firstVisible, and
|
||||||
|
* lastVisible.
|
||||||
|
*/
|
||||||
|
private _notifyRange;
|
||||||
|
private _notifyVisibility;
|
||||||
|
/**
|
||||||
|
* Render and update the view at the next opportunity with the given
|
||||||
|
* container size.
|
||||||
|
*/
|
||||||
|
private _containerSizeChanged;
|
||||||
|
private _observeMutations;
|
||||||
|
private _childLoaded;
|
||||||
|
private _childrenSizeChanged;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=VirtualScroller.d.ts.map
|
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"VirtualScroller.d.ts","sourceRoot":"","sources":["../../../src/lib/uni-virtualizer/lib/VirtualScroller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAW,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEnG,eAAO,MAAM,WAAW,eAAwB,CAAC;AAYjD,oBAAY,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,UAAU,8BAA+B,SAAQ,OAAO;IACtD,CAAC,WAAW,CAAC,CAAC,EAAE,eAAe,CAAA;CAChC;AAED,UAAU,iCAAkC,SAAQ,UAAU;IAC5D,CAAC,WAAW,CAAC,CAAC,EAAE,eAAe,CAAA;CAChC;AAED,aAAK,SAAS,GAAG,8BAA8B,GAAG,iCAAiC,CAAC;AACpF,oBAAY,gBAAgB,GAAG,8BAA8B,CAAC;AAM9D,aAAK,iBAAiB,GAAG;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAC,CAAC;AAElD,oBAAY,kBAAkB,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,CAAC;IAEtD;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhC;;OAEG;IACH,SAAS,EAAE,OAAO,GAAG,UAAU,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,eAAe,CAAuB;IAC9C;;;OAGG;IAGH,OAAO,CAAC,OAAO,CAAuB;IAEtC;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAwB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAA4B;IAE1C;;;OAGG;IACH,OAAO,CAAC,WAAW,CAA2B;IAE9C;;;OAGG;IACH,OAAO,CAAC,UAAU,CAA4C;IAE9D;;OAEG;IACH,OAAO,CAAC,YAAY,CAAmD;IAGvE,OAAO,CAAC,kBAAkB,CAAkC;IAE5D,OAAO,CAAC,aAAa,CAAwC;IAE7D,OAAO,CAAC,aAAa,CAAQ;IAE7B,OAAO,CAAC,aAAa,CAAQ;IAE7B,OAAO,CAAC,kBAAkB,CAAQ;IAElC;;OAEG;IACH,SAAS,CAAC,UAAU,EAAE,SAAS,GAAG,IAAI,CAAQ;IAE9C;;OAEG;IACH,OAAO,CAAC,iBAAiB,CAAiC;IAE1D;;;OAGG;IACH,OAAO,CAAC,qBAAqB,CAAuB;IAEpD;;OAEG;IACH,OAAO,CAAC,cAAc,CAAgD;IAEtE;;OAEG;IACH,OAAO,CAAC,YAAY,CAA+B;IAEnD;;OAEG;IACH,OAAO,CAAC,WAAW,CAA+B;IAElD,OAAO,CAAC,iBAAiB,CAAiC;IAC1D,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,wBAAwB,CAAyB;IACzD,OAAO,CAAC,kBAAkB,CAAS;IAInC,OAAO,CAAC,aAAa,CAAgC;IAErD;;OAEG;IACH,OAAO,CAAC,cAAc,CAA4B;IAElD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAsB;IAEpC;;OAEG;IACH,OAAO,CAAC,WAAW,CAAuB;IAE1C;;;OAGG;IACH,SAAS,CAAC,MAAM,SAAK;IAErB;;OAEG;IACH,SAAS,CAAC,KAAK,SAAK;IAEpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAAK;IAE1B;;OAEG;IACH,OAAO,CAAC,YAAY,CAAK;IAEzB,SAAS,CAAC,UAAU,kBAAiB;IAErC;;;;OAIG;IACF,SAAS,CAAC,gBAAgB,EAAE,CAAC,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IAE/E,SAAS,CAAC,qBAAqB,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAQ;gBAEnF,MAAM,CAAC,EAAE,qBAAqB;IAS1C,IAAI,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,EAM1C;IAED;;;OAGG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,UAAU,CAAC,GAAG,EAAE,MAAM,EAWzB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,SAAS,GAAG,IAAI,CAEhC;IAED,IAAI,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,EA6ExC;IAID,IAAI,MAAM,IAAI,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAEhE;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,IAAI,EAiErE;IAID,iBAAiB;IAMjB,gBAAgB;;;;IAchB,OAAO,CAAC,gBAAgB;IAgBxB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAQxC;;;;OAIG;IACH,IAAI,YAAY,IAAI,OAAO,GAAG,MAAM,GAAG,IAAI,CAE1C;IACD,IAAI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,EAyB/C;IAED;;;OAGG;IACH,IAAI,aAAa,CAAC,QAAQ,EAAE,kBAAkB,EAG7C;cAEe,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IASpD,UAAU;IA0BhB,aAAa;IAoBb,OAAO,CAAC,kBAAkB;IAgB1B,WAAW,CAAC,KAAK,EAAE,WAAW;YA4BhB,oBAAoB;IAWlC,OAAO,CAAC,qBAAqB;IAgB7B,IAAI,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,CAWlC;IAED,OAAO,CAAC,WAAW;IAiDnB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;YAsBX,YAAY;IAkB1B,OAAO,CAAC,mBAAmB;IAS3B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;YAMf,iBAAiB;IAc/B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,oBAAoB;CAO7B"}
|
@@ -0,0 +1,725 @@
|
|||||||
|
import getResizeObserver from "./polyfillLoaders/ResizeObserver.js";
|
||||||
|
export const scrollerRef = Symbol("scrollerRef");
|
||||||
|
/**
|
||||||
|
* Provides virtual scrolling boilerplate.
|
||||||
|
*
|
||||||
|
* Extensions of this class must set container, layout, and scrollTarget.
|
||||||
|
*
|
||||||
|
* Extensions of this class must also override VirtualRepeater's DOM
|
||||||
|
* manipulation methods.
|
||||||
|
*/
|
||||||
|
export class VirtualScroller {
|
||||||
|
constructor(config) {
|
||||||
|
this._benchmarkStart = null;
|
||||||
|
/**
|
||||||
|
* Whether the layout should receive an updated viewport size on the next
|
||||||
|
* render.
|
||||||
|
*/
|
||||||
|
// private _needsUpdateView: boolean = false;
|
||||||
|
this._layout = null;
|
||||||
|
/**
|
||||||
|
* The element that generates scroll events and defines the container
|
||||||
|
* viewport. Set by scrollTarget.
|
||||||
|
*/
|
||||||
|
this._scrollTarget = null;
|
||||||
|
/**
|
||||||
|
* A sentinel element that sizes the container when it is a scrolling
|
||||||
|
* element. This ensures the scroll bar accurately reflects the total
|
||||||
|
* size of the list.
|
||||||
|
*/
|
||||||
|
this._sizer = null;
|
||||||
|
/**
|
||||||
|
* Layout provides these values, we set them on _render().
|
||||||
|
* TODO @straversi: Can we find an XOR type, usable for the key here?
|
||||||
|
*/
|
||||||
|
this._scrollSize = null;
|
||||||
|
/**
|
||||||
|
* Difference between scroll target's current and required scroll offsets.
|
||||||
|
* Provided by layout.
|
||||||
|
*/
|
||||||
|
this._scrollErr = null;
|
||||||
|
/**
|
||||||
|
* A list of the positions (top, left) of the children in the current range.
|
||||||
|
*/
|
||||||
|
this._childrenPos = null;
|
||||||
|
// TODO: (graynorton): type
|
||||||
|
this._childMeasurements = null;
|
||||||
|
this._toBeMeasured = new Map();
|
||||||
|
this._rangeChanged = true;
|
||||||
|
this._itemsChanged = true;
|
||||||
|
this._visibilityChanged = true;
|
||||||
|
/**
|
||||||
|
* Containing element. Set by container.
|
||||||
|
*/
|
||||||
|
this._container = null;
|
||||||
|
/**
|
||||||
|
* The parent of all child nodes to be rendered. Set by container.
|
||||||
|
*/
|
||||||
|
this._containerElement = null;
|
||||||
|
/**
|
||||||
|
* Keep track of original inline style of the container, so it can be
|
||||||
|
* restored when container is changed.
|
||||||
|
*/
|
||||||
|
this._containerInlineStyle = null;
|
||||||
|
/**
|
||||||
|
* Size of the container.
|
||||||
|
*/
|
||||||
|
this._containerSize = null;
|
||||||
|
/**
|
||||||
|
* Resize observer attached to container.
|
||||||
|
*/
|
||||||
|
this._containerRO = null;
|
||||||
|
/**
|
||||||
|
* Resize observer attached to children.
|
||||||
|
*/
|
||||||
|
this._childrenRO = null;
|
||||||
|
this._mutationObserver = null;
|
||||||
|
this._mutationPromise = null;
|
||||||
|
this._mutationPromiseResolver = null;
|
||||||
|
this._mutationsObserved = false;
|
||||||
|
// TODO (graynorton): Rethink, per longer comment below
|
||||||
|
this._loadListener = this._childLoaded.bind(this);
|
||||||
|
/**
|
||||||
|
* Index and position of item to scroll to.
|
||||||
|
*/
|
||||||
|
this._scrollToIndex = null;
|
||||||
|
/**
|
||||||
|
* Items to render. Set by items.
|
||||||
|
*/
|
||||||
|
this._items = [];
|
||||||
|
/**
|
||||||
|
* Total number of items to render. Set by totalItems.
|
||||||
|
*/
|
||||||
|
this._totalItems = null;
|
||||||
|
/**
|
||||||
|
* Index of the first child in the range, not necessarily the first visible child.
|
||||||
|
* TODO @straversi: Consider renaming these.
|
||||||
|
*/
|
||||||
|
this._first = 0;
|
||||||
|
/**
|
||||||
|
* Index of the last child in the range.
|
||||||
|
*/
|
||||||
|
this._last = 0;
|
||||||
|
/**
|
||||||
|
* Index of the first item intersecting the container element.
|
||||||
|
*/
|
||||||
|
this._firstVisible = 0;
|
||||||
|
/**
|
||||||
|
* Index of the last item intersecting the container element.
|
||||||
|
*/
|
||||||
|
this._lastVisible = 0;
|
||||||
|
this._scheduled = new WeakSet();
|
||||||
|
/**
|
||||||
|
* Invoked at the end of each render cycle: children in the range are
|
||||||
|
* measured, and their dimensions passed to this callback. Use it to layout
|
||||||
|
* children as needed.
|
||||||
|
*/
|
||||||
|
this._measureCallback = null;
|
||||||
|
this._measureChildOverride = null;
|
||||||
|
this._first = -1;
|
||||||
|
this._last = -1;
|
||||||
|
if (config) {
|
||||||
|
Object.assign(this, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set items(items) {
|
||||||
|
if (Array.isArray(items) && items !== this._items) {
|
||||||
|
this._itemsChanged = true;
|
||||||
|
this._items = items;
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The total number of items, regardless of the range, that can be rendered
|
||||||
|
* as child nodes.
|
||||||
|
*/
|
||||||
|
get totalItems() {
|
||||||
|
return this._totalItems === null ? this._items.length : this._totalItems;
|
||||||
|
}
|
||||||
|
set totalItems(num) {
|
||||||
|
if (typeof num !== "number" && num !== null) {
|
||||||
|
throw new Error("New value must be a number.");
|
||||||
|
}
|
||||||
|
// TODO(valdrin) should we check if it is a finite number?
|
||||||
|
// Technically, Infinity would break Layout, not VirtualRepeater.
|
||||||
|
if (num !== this._totalItems) {
|
||||||
|
this._totalItems = num;
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The parent of all child nodes to be rendered.
|
||||||
|
*/
|
||||||
|
get container() {
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
set container(container) {
|
||||||
|
if (container === this._container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._container) {
|
||||||
|
// Remove children from old container.
|
||||||
|
// TODO (graynorton): Decide whether we'd rather fire an event to clear
|
||||||
|
// the range and let the renderer take care of removing the DOM children
|
||||||
|
this._children.forEach((child) => child.parentNode.removeChild(child));
|
||||||
|
}
|
||||||
|
this._container = container;
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
this._initResizeObservers().then(() => {
|
||||||
|
const oldEl = this._containerElement;
|
||||||
|
// Consider document fragments as shadowRoots.
|
||||||
|
const newEl =
|
||||||
|
container && container.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
||||||
|
? container.host
|
||||||
|
: container;
|
||||||
|
if (oldEl === newEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._containerRO.disconnect();
|
||||||
|
this._containerSize = null;
|
||||||
|
if (oldEl) {
|
||||||
|
if (this._containerInlineStyle) {
|
||||||
|
oldEl.setAttribute("style", this._containerInlineStyle);
|
||||||
|
} else {
|
||||||
|
oldEl.removeAttribute("style");
|
||||||
|
}
|
||||||
|
this._containerInlineStyle = null;
|
||||||
|
if (oldEl === this._scrollTarget) {
|
||||||
|
oldEl.removeEventListener("scroll", this, { passive: true });
|
||||||
|
this._sizer && this._sizer.remove();
|
||||||
|
}
|
||||||
|
oldEl.removeEventListener("load", this._loadListener, true);
|
||||||
|
this._mutationObserver.disconnect();
|
||||||
|
} else {
|
||||||
|
// First time container was setup, add listeners only now.
|
||||||
|
addEventListener("scroll", this, { passive: true });
|
||||||
|
}
|
||||||
|
this._containerElement = newEl;
|
||||||
|
if (newEl) {
|
||||||
|
this._containerInlineStyle = newEl.getAttribute("style") || null;
|
||||||
|
// https://github.com/PolymerLabs/uni-virtualizer/issues/104
|
||||||
|
// Would rather set these CSS properties on the host using Shadow Root
|
||||||
|
// style scoping (and fall back to a global stylesheet where native
|
||||||
|
// Shadow DOM is not available), but this Mobile Safari bug is preventing
|
||||||
|
// that from working: https://bugs.webkit.org/show_bug.cgi?id=226195
|
||||||
|
const style = newEl.style;
|
||||||
|
style.display = style.display || "block";
|
||||||
|
style.position = style.position || "relative";
|
||||||
|
style.overflow = style.overflow || "auto";
|
||||||
|
style.contain = style.contain || "strict";
|
||||||
|
if (newEl === this._scrollTarget) {
|
||||||
|
this._sizer = this._sizer || this._createContainerSizer();
|
||||||
|
this._container.insertBefore(this._sizer, this._container.firstChild);
|
||||||
|
}
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
this._containerRO.observe(newEl);
|
||||||
|
this._mutationObserver.observe(newEl, { childList: true });
|
||||||
|
this._mutationPromise = new Promise(
|
||||||
|
(resolve) => (this._mutationPromiseResolver = resolve)
|
||||||
|
);
|
||||||
|
if (this._layout && this._layout.listenForChildLoadEvents) {
|
||||||
|
newEl.addEventListener("load", this._loadListener, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// This will always actually return a layout instance,
|
||||||
|
// but TypeScript wants the getter and setter types to be the same
|
||||||
|
get layout() {
|
||||||
|
return this._layout;
|
||||||
|
}
|
||||||
|
set layout(layout) {
|
||||||
|
if (this._layout === layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _layout = null;
|
||||||
|
let _config = {};
|
||||||
|
if (typeof layout === "object") {
|
||||||
|
if (layout.type !== undefined) {
|
||||||
|
_layout = layout.type;
|
||||||
|
// delete (layout as LayoutSpecifier).type;
|
||||||
|
}
|
||||||
|
_config = layout;
|
||||||
|
} else {
|
||||||
|
_layout = layout;
|
||||||
|
}
|
||||||
|
if (typeof _layout === "function") {
|
||||||
|
if (this._layout instanceof _layout) {
|
||||||
|
if (_config) {
|
||||||
|
this._layout.config = _config;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
_layout = new _layout(_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._layout) {
|
||||||
|
this._measureCallback = null;
|
||||||
|
this._measureChildOverride = null;
|
||||||
|
this._layout.removeEventListener("scrollsizechange", this);
|
||||||
|
this._layout.removeEventListener("scrollerrorchange", this);
|
||||||
|
this._layout.removeEventListener("itempositionchange", this);
|
||||||
|
this._layout.removeEventListener("rangechange", this);
|
||||||
|
delete this.container[scrollerRef];
|
||||||
|
this.container.removeEventListener("load", this._loadListener, true);
|
||||||
|
// Reset container size so layout can get correct viewport size.
|
||||||
|
if (this._containerElement) {
|
||||||
|
this._sizeContainer(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._layout = _layout;
|
||||||
|
if (this._layout) {
|
||||||
|
if (
|
||||||
|
this._layout.measureChildren &&
|
||||||
|
typeof this._layout.updateItemSizes === "function"
|
||||||
|
) {
|
||||||
|
if (typeof this._layout.measureChildren === "function") {
|
||||||
|
this._measureChildOverride = this._layout.measureChildren;
|
||||||
|
}
|
||||||
|
this._measureCallback = this._layout.updateItemSizes.bind(this._layout);
|
||||||
|
}
|
||||||
|
this._layout.addEventListener("scrollsizechange", this);
|
||||||
|
this._layout.addEventListener("scrollerrorchange", this);
|
||||||
|
this._layout.addEventListener("itempositionchange", this);
|
||||||
|
this._layout.addEventListener("rangechange", this);
|
||||||
|
this._container[scrollerRef] = this;
|
||||||
|
if (this._layout.listenForChildLoadEvents) {
|
||||||
|
this._container.addEventListener("load", this._loadListener, true);
|
||||||
|
}
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO (graynorton): Rework benchmarking so that it has no API and
|
||||||
|
// instead is always on except in production builds
|
||||||
|
startBenchmarking() {
|
||||||
|
if (this._benchmarkStart === null) {
|
||||||
|
this._benchmarkStart = window.performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopBenchmarking() {
|
||||||
|
if (this._benchmarkStart !== null) {
|
||||||
|
const now = window.performance.now();
|
||||||
|
const timeElapsed = now - this._benchmarkStart;
|
||||||
|
const entries = performance.getEntriesByName(
|
||||||
|
"uv-virtualizing",
|
||||||
|
"measure"
|
||||||
|
);
|
||||||
|
const virtualizationTime = entries
|
||||||
|
.filter((e) => e.startTime >= this._benchmarkStart && e.startTime < now)
|
||||||
|
.reduce((t, m) => t + m.duration, 0);
|
||||||
|
this._benchmarkStart = null;
|
||||||
|
return { timeElapsed, virtualizationTime };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_measureChildren() {
|
||||||
|
const mm = {};
|
||||||
|
const children = this._children;
|
||||||
|
const fn = this._measureChildOverride || this._measureChild;
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const child = children[i];
|
||||||
|
const idx = this._first + i;
|
||||||
|
if (this._itemsChanged || this._toBeMeasured.has(child)) {
|
||||||
|
mm[idx] = fn.call(
|
||||||
|
this,
|
||||||
|
child,
|
||||||
|
this._items[idx] /*as unknown as object*/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._childMeasurements = mm;
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
this._toBeMeasured.clear();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the width, height, and margins of the given child.
|
||||||
|
*/
|
||||||
|
_measureChild(element) {
|
||||||
|
// offsetWidth doesn't take transforms in consideration, so we use
|
||||||
|
// getBoundingClientRect which does.
|
||||||
|
const { width, height } = element.getBoundingClientRect();
|
||||||
|
return Object.assign({ width, height }, getMargins(element));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The element that generates scroll events and defines the container
|
||||||
|
* viewport. The value `null` (default) corresponds to `window` as scroll
|
||||||
|
* target.
|
||||||
|
*/
|
||||||
|
get scrollTarget() {
|
||||||
|
return this._scrollTarget;
|
||||||
|
}
|
||||||
|
set scrollTarget(target) {
|
||||||
|
// Consider window as null.
|
||||||
|
if (target === window) {
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
if (this._scrollTarget === target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._sizeContainer(undefined);
|
||||||
|
if (this._scrollTarget) {
|
||||||
|
this._scrollTarget.removeEventListener("scroll", this, { passive: true });
|
||||||
|
if (this._sizer && this._scrollTarget === this._containerElement) {
|
||||||
|
this._sizer.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._scrollTarget = target;
|
||||||
|
if (target) {
|
||||||
|
target.addEventListener("scroll", this, { passive: true });
|
||||||
|
if (target === this._containerElement) {
|
||||||
|
this._sizer = this._sizer || this._createContainerSizer();
|
||||||
|
this._container.insertBefore(this._sizer, this._container.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Index and position of item to scroll to. The scroller will fix to that point
|
||||||
|
* until the user scrolls.
|
||||||
|
*/
|
||||||
|
set scrollToIndex(newValue) {
|
||||||
|
this._scrollToIndex = newValue;
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
async _schedule(method) {
|
||||||
|
if (!this._scheduled.has(method)) {
|
||||||
|
this._scheduled.add(method);
|
||||||
|
await Promise.resolve();
|
||||||
|
this._scheduled.delete(method);
|
||||||
|
method.call(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _updateDOM() {
|
||||||
|
const { _rangeChanged, _itemsChanged } = this;
|
||||||
|
if (this._visibilityChanged) {
|
||||||
|
this._notifyVisibility();
|
||||||
|
this._visibilityChanged = false;
|
||||||
|
}
|
||||||
|
if (_rangeChanged || _itemsChanged) {
|
||||||
|
this._notifyRange();
|
||||||
|
this._rangeChanged = false;
|
||||||
|
this._itemsChanged = false;
|
||||||
|
await this._mutationPromise;
|
||||||
|
}
|
||||||
|
if (this._layout.measureChildren) {
|
||||||
|
this._children.forEach((child) => this._childrenRO.observe(child));
|
||||||
|
}
|
||||||
|
this._positionChildren(this._childrenPos);
|
||||||
|
this._sizeContainer(this._scrollSize);
|
||||||
|
if (this._scrollErr) {
|
||||||
|
this._correctScrollError(this._scrollErr);
|
||||||
|
this._scrollErr = null;
|
||||||
|
}
|
||||||
|
if (this._benchmarkStart && "mark" in window.performance) {
|
||||||
|
window.performance.mark("uv-end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_updateLayout() {
|
||||||
|
this._layout.totalItems = this._totalItems;
|
||||||
|
if (this._scrollToIndex !== null) {
|
||||||
|
this._layout.scrollToIndex(
|
||||||
|
this._scrollToIndex.index,
|
||||||
|
this._scrollToIndex.position
|
||||||
|
);
|
||||||
|
this._scrollToIndex = null;
|
||||||
|
}
|
||||||
|
this._updateView();
|
||||||
|
if (this._childMeasurements !== null) {
|
||||||
|
// If the layout has been changed, we may have measurements but no callback
|
||||||
|
if (this._measureCallback) {
|
||||||
|
this._measureCallback(this._childMeasurements);
|
||||||
|
}
|
||||||
|
this._childMeasurements = null;
|
||||||
|
}
|
||||||
|
this._layout.reflowIfNeeded(this._itemsChanged);
|
||||||
|
if (this._benchmarkStart && "mark" in window.performance) {
|
||||||
|
window.performance.mark("uv-end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handleScrollEvent() {
|
||||||
|
if (this._benchmarkStart && "mark" in window.performance) {
|
||||||
|
try {
|
||||||
|
window.performance.measure("uv-virtualizing", "uv-start", "uv-end");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error measuring performance data: ", e);
|
||||||
|
}
|
||||||
|
window.performance.mark("uv-start");
|
||||||
|
}
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
handleEvent(event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "scroll":
|
||||||
|
if (!this._scrollTarget || event.target === this._scrollTarget) {
|
||||||
|
this._handleScrollEvent();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "scrollsizechange":
|
||||||
|
this._scrollSize = event.detail;
|
||||||
|
this._schedule(this._updateDOM);
|
||||||
|
break;
|
||||||
|
case "scrollerrorchange":
|
||||||
|
this._scrollErr = event.detail;
|
||||||
|
this._schedule(this._updateDOM);
|
||||||
|
break;
|
||||||
|
case "itempositionchange":
|
||||||
|
this._childrenPos = event.detail;
|
||||||
|
this._schedule(this._updateDOM);
|
||||||
|
break;
|
||||||
|
case "rangechange":
|
||||||
|
this._adjustRange(event.detail);
|
||||||
|
this._schedule(this._updateDOM);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("event not handled", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _initResizeObservers() {
|
||||||
|
if (this._containerRO === null) {
|
||||||
|
const ResizeObserver = await getResizeObserver();
|
||||||
|
this._containerRO = new ResizeObserver((entries) =>
|
||||||
|
this._containerSizeChanged(entries[0].contentRect)
|
||||||
|
);
|
||||||
|
this._childrenRO = new ResizeObserver(
|
||||||
|
this._childrenSizeChanged.bind(this)
|
||||||
|
);
|
||||||
|
this._mutationObserver = new MutationObserver(
|
||||||
|
this._observeMutations.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_createContainerSizer() {
|
||||||
|
const sizer = document.createElement("div");
|
||||||
|
// When the scrollHeight is large, the height of this element might be
|
||||||
|
// ignored. Setting content and font-size ensures the element has a size.
|
||||||
|
Object.assign(sizer.style, {
|
||||||
|
position: "absolute",
|
||||||
|
margin: "-2px 0 0 0",
|
||||||
|
padding: 0,
|
||||||
|
visibility: "hidden",
|
||||||
|
fontSize: "2px",
|
||||||
|
});
|
||||||
|
sizer.innerHTML = " ";
|
||||||
|
sizer.id = "uni-virtualizer-spacer";
|
||||||
|
return sizer;
|
||||||
|
}
|
||||||
|
get _children() {
|
||||||
|
const arr = [];
|
||||||
|
let next = this.container.firstElementChild;
|
||||||
|
while (next) {
|
||||||
|
// Skip our spacer. TODO (graynorton): Feels a bit hacky. Anything better?
|
||||||
|
if (next.id !== "uni-virtualizer-spacer") {
|
||||||
|
arr.push(next);
|
||||||
|
}
|
||||||
|
next = next.nextElementSibling;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
_updateView() {
|
||||||
|
if (!this.container || !this._containerElement || !this._layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let width, height, top, left;
|
||||||
|
if (
|
||||||
|
this._scrollTarget === this._containerElement &&
|
||||||
|
this._containerSize !== null
|
||||||
|
) {
|
||||||
|
width = this._containerSize.width;
|
||||||
|
height = this._containerSize.height;
|
||||||
|
left = this._containerElement.scrollLeft;
|
||||||
|
top = this._containerElement.scrollTop;
|
||||||
|
} else {
|
||||||
|
const containerBounds = this._containerElement.getBoundingClientRect();
|
||||||
|
const scrollBounds = this._scrollTarget
|
||||||
|
? this._scrollTarget.getBoundingClientRect()
|
||||||
|
: {
|
||||||
|
top: containerBounds.top + window.pageYOffset,
|
||||||
|
left: containerBounds.left + window.pageXOffset,
|
||||||
|
width: innerWidth,
|
||||||
|
height: innerHeight,
|
||||||
|
};
|
||||||
|
const scrollerWidth = scrollBounds.width;
|
||||||
|
const scrollerHeight = scrollBounds.height;
|
||||||
|
const xMin = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(scrollerWidth, containerBounds.left - scrollBounds.left)
|
||||||
|
);
|
||||||
|
const yMin = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(scrollerHeight, containerBounds.top - scrollBounds.top)
|
||||||
|
);
|
||||||
|
// TODO (graynorton): Direction is intended to be a layout-level concept, not a scroller-level concept,
|
||||||
|
// so this feels like a factoring problem
|
||||||
|
const xMax =
|
||||||
|
this._layout.direction === "vertical"
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(scrollerWidth, containerBounds.right - scrollBounds.left)
|
||||||
|
)
|
||||||
|
: scrollerWidth;
|
||||||
|
const yMax =
|
||||||
|
this._layout.direction === "vertical"
|
||||||
|
? scrollerHeight
|
||||||
|
: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
scrollerHeight,
|
||||||
|
containerBounds.bottom - scrollBounds.top
|
||||||
|
)
|
||||||
|
);
|
||||||
|
width = xMax - xMin;
|
||||||
|
height = yMax - yMin;
|
||||||
|
left = Math.max(0, -(containerBounds.left - scrollBounds.left));
|
||||||
|
top = Math.max(0, -(containerBounds.top - scrollBounds.top));
|
||||||
|
}
|
||||||
|
this._layout.viewportSize = { width, height };
|
||||||
|
this._layout.viewportScroll = { top, left };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Styles the _sizer element or the container so that its size reflects the
|
||||||
|
* total size of all items.
|
||||||
|
*/
|
||||||
|
_sizeContainer(size) {
|
||||||
|
if (this._scrollTarget === this._containerElement) {
|
||||||
|
const left = size && size.width ? size.width - 1 : 0;
|
||||||
|
const top = size && size.height ? size.height - 1 : 0;
|
||||||
|
if (this._sizer) {
|
||||||
|
this._sizer.style.transform = `translate(${left}px, ${top}px)`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this._containerElement) {
|
||||||
|
const style = this._containerElement.style;
|
||||||
|
style.minWidth = size && size.width ? size.width + "px" : null;
|
||||||
|
style.minHeight = size && size.height ? size.height + "px" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the top and left transform style of the children from the values in
|
||||||
|
* pos.
|
||||||
|
*/
|
||||||
|
_positionChildren(pos) {
|
||||||
|
if (pos) {
|
||||||
|
const children = this._children;
|
||||||
|
Object.keys(pos).forEach((key) => {
|
||||||
|
const idx = key - this._first;
|
||||||
|
const child = children[idx];
|
||||||
|
if (child) {
|
||||||
|
const { top, left, width, height } = pos[key];
|
||||||
|
child.style.position = "absolute";
|
||||||
|
child.style.boxSizing = "border-box";
|
||||||
|
child.style.transform = `translate(${left}px, ${top}px)`;
|
||||||
|
if (width !== undefined) {
|
||||||
|
child.style.width = width + "px";
|
||||||
|
}
|
||||||
|
if (height !== undefined) {
|
||||||
|
child.style.height = height + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _adjustRange(range) {
|
||||||
|
const { _first, _last, _firstVisible, _lastVisible } = this;
|
||||||
|
this._first = range.first;
|
||||||
|
this._last = range.last;
|
||||||
|
this._firstVisible = range.firstVisible;
|
||||||
|
this._lastVisible = range.lastVisible;
|
||||||
|
this._rangeChanged =
|
||||||
|
this._rangeChanged || this._first !== _first || this._last !== _last;
|
||||||
|
this._visibilityChanged =
|
||||||
|
this._visibilityChanged ||
|
||||||
|
this._firstVisible !== _firstVisible ||
|
||||||
|
this._lastVisible !== _lastVisible;
|
||||||
|
}
|
||||||
|
_correctScrollError(err) {
|
||||||
|
if (this._scrollTarget) {
|
||||||
|
this._scrollTarget.scrollTop -= err.top;
|
||||||
|
this._scrollTarget.scrollLeft -= err.left;
|
||||||
|
} else {
|
||||||
|
window.scroll(
|
||||||
|
window.pageXOffset - err.left,
|
||||||
|
window.pageYOffset - err.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Emits a rangechange event with the current first, last, firstVisible, and
|
||||||
|
* lastVisible.
|
||||||
|
*/
|
||||||
|
_notifyRange() {
|
||||||
|
// TODO (graynorton): Including visibility here for backward compat, but
|
||||||
|
// may decide to remove at some point. The rationale for separating is that
|
||||||
|
// range change events are mainly intended for "internal" consumption by the
|
||||||
|
// renderer, whereas visibility change events are mainly intended for "external"
|
||||||
|
// consumption by application code.
|
||||||
|
this._container.dispatchEvent(
|
||||||
|
new CustomEvent("rangeChanged", {
|
||||||
|
detail: {
|
||||||
|
first: this._first,
|
||||||
|
last: this._last,
|
||||||
|
firstVisible: this._firstVisible,
|
||||||
|
lastVisible: this._lastVisible,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_notifyVisibility() {
|
||||||
|
this._container.dispatchEvent(
|
||||||
|
new CustomEvent("visibilityChanged", {
|
||||||
|
detail: {
|
||||||
|
first: this._first,
|
||||||
|
last: this._last,
|
||||||
|
firstVisible: this._firstVisible,
|
||||||
|
lastVisible: this._lastVisible,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Render and update the view at the next opportunity with the given
|
||||||
|
* container size.
|
||||||
|
*/
|
||||||
|
_containerSizeChanged(size) {
|
||||||
|
const { width, height } = size;
|
||||||
|
this._containerSize = { width, height };
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
async _observeMutations() {
|
||||||
|
if (!this._mutationsObserved) {
|
||||||
|
this._mutationsObserved = true;
|
||||||
|
this._mutationPromiseResolver();
|
||||||
|
this._mutationPromise = new Promise(
|
||||||
|
(resolve) => (this._mutationPromiseResolver = resolve)
|
||||||
|
);
|
||||||
|
this._mutationsObserved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO (graynorton): Rethink how this works. Probably child loading is too specific
|
||||||
|
// to have dedicated support for; might want some more generic lifecycle hooks for
|
||||||
|
// layouts to use. Possibly handle measurement this way, too, or maybe that remains
|
||||||
|
// a first-class feature?
|
||||||
|
_childLoaded() {
|
||||||
|
// this.requestRemeasure();
|
||||||
|
}
|
||||||
|
_childrenSizeChanged(changes) {
|
||||||
|
for (const change of changes) {
|
||||||
|
this._toBeMeasured.set(change.target, change.contentRect);
|
||||||
|
}
|
||||||
|
this._measureChildren();
|
||||||
|
this._schedule(this._updateLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getMargins(el) {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return {
|
||||||
|
marginTop: getMarginValue(style.marginTop),
|
||||||
|
marginRight: getMarginValue(style.marginRight),
|
||||||
|
marginBottom: getMarginValue(style.marginBottom),
|
||||||
|
marginLeft: getMarginValue(style.marginLeft),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getMarginValue(value) {
|
||||||
|
const float = value ? parseFloat(value) : NaN;
|
||||||
|
return Number.isNaN(float) ? 0 : float;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=VirtualScroller.js.map
|
File diff suppressed because one or more lines are too long
90
src/resources/lit-virtualizer/lib/uni-virtualizer/lib/layouts/Layout.d.ts
vendored
Normal file
90
src/resources/lit-virtualizer/lib/uni-virtualizer/lib/layouts/Layout.d.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export declare type dimension = "height" | "width";
|
||||||
|
export declare type Size = {
|
||||||
|
[key in dimension]: number;
|
||||||
|
};
|
||||||
|
export declare type Margins = {
|
||||||
|
marginTop: number;
|
||||||
|
marginRight: number;
|
||||||
|
marginBottom: number;
|
||||||
|
marginLeft: number;
|
||||||
|
};
|
||||||
|
export declare type ItemBox = Size | (Size & Margins);
|
||||||
|
export declare type position = "left" | "top";
|
||||||
|
export declare type Positions = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
export declare type LayoutConstructor = new (config?: object) => Layout;
|
||||||
|
export interface LayoutSpecifier {
|
||||||
|
type: LayoutConstructor;
|
||||||
|
}
|
||||||
|
export declare type LayoutSpecifierFactory = (
|
||||||
|
config?: object
|
||||||
|
) => LayoutSpecifier;
|
||||||
|
export declare type ScrollDirection = "vertical" | "horizontal";
|
||||||
|
/**
|
||||||
|
* Interface for layouts consumed by VirtualScroller or VirtualRepeater.
|
||||||
|
*/
|
||||||
|
export interface Layout {
|
||||||
|
config?: object;
|
||||||
|
totalItems: number;
|
||||||
|
direction: ScrollDirection;
|
||||||
|
viewportSize: Size;
|
||||||
|
viewportScroll: Positions;
|
||||||
|
readonly measureChildren?: boolean | ((e: Element, i: unknown) => ItemBox);
|
||||||
|
readonly listenForChildLoadEvents?: boolean;
|
||||||
|
updateItemSizes?: (sizes: { [key: number]: ItemBox }) => void;
|
||||||
|
addEventListener: Function;
|
||||||
|
removeEventListener: Function;
|
||||||
|
scrollToIndex: (index: number, position: string) => void;
|
||||||
|
/**
|
||||||
|
* Called by a VirtualRepeater or VirtualScroller when an update that
|
||||||
|
* potentially affects layout has occurred. For example, a viewport size
|
||||||
|
* change.
|
||||||
|
*
|
||||||
|
* The layout is in turn responsible for dispatching events, as necessary,
|
||||||
|
* to the VirtualRepeater or VirtualScroller. Each of the following events
|
||||||
|
* represents an update that should be determined during a reflow. Dispatch
|
||||||
|
* each event at maximum once during a single reflow.
|
||||||
|
*
|
||||||
|
* Events that should be dispatched:
|
||||||
|
* - scrollsizechange
|
||||||
|
* Dispatch when the total length of all items in the scrolling direction,
|
||||||
|
* including spacing, changes.
|
||||||
|
* detail: {
|
||||||
|
* 'height' | 'width': number
|
||||||
|
* }
|
||||||
|
* - rangechange
|
||||||
|
* Dispatch when the range of children that should be displayed changes
|
||||||
|
* (based on layout calculations and the size of the container) or when
|
||||||
|
* the first or last item to intersect the container changes.
|
||||||
|
* detail: {
|
||||||
|
* first: number,
|
||||||
|
* last: number,
|
||||||
|
* num: number,
|
||||||
|
* stable: boolean,
|
||||||
|
* remeasure: boolean,
|
||||||
|
* firstVisible: number,
|
||||||
|
* lastVisible: number,
|
||||||
|
* }
|
||||||
|
* - itempositionchange
|
||||||
|
* Dispatch when the child positions change, for example due to a range
|
||||||
|
* change.
|
||||||
|
* detail {
|
||||||
|
* [number]: {
|
||||||
|
* left: number,
|
||||||
|
* top: number
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* - scrollerrorchange
|
||||||
|
* Dispatch when the set viewportScroll offset is not what it should be.
|
||||||
|
* detail {
|
||||||
|
* height: number,
|
||||||
|
* width: number,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
reflowIfNeeded: (force: boolean) => void;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=Layout.d.ts.map
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user