mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-30 03:29:27 +00:00
Compare commits
101 Commits
quickbar-n
...
Add-Redire
Author | SHA1 | Date | |
---|---|---|---|
![]() |
191f81d9fe | ||
![]() |
2751f8f33b | ||
![]() |
57f2df3b3e | ||
![]() |
6822f0d067 | ||
![]() |
cfba957313 | ||
![]() |
3149ffbf19 | ||
![]() |
4cd8b76d7e | ||
![]() |
4b644d8bc5 | ||
![]() |
307cd5ad8c | ||
![]() |
ebc807a6a4 | ||
![]() |
66adecdfc9 | ||
![]() |
2cc6432a0f | ||
![]() |
a2c0c0474a | ||
![]() |
27884b9a54 | ||
![]() |
293df61872 | ||
![]() |
f82dada3e5 | ||
![]() |
e5824c4794 | ||
![]() |
186550229c | ||
![]() |
7877dd8e6b | ||
![]() |
b03abc249b | ||
![]() |
fda03918b9 | ||
![]() |
6747375a1b | ||
![]() |
53b6e31881 | ||
![]() |
fa004de2d1 | ||
![]() |
3605f7b70f | ||
![]() |
5348c54c91 | ||
![]() |
684e4421bc | ||
![]() |
28f5611df5 | ||
![]() |
8da73d49d7 | ||
![]() |
049ddd5f84 | ||
![]() |
8ae2d4e93a | ||
![]() |
824bb9ba35 | ||
![]() |
d550b1a18e | ||
![]() |
dea6c0e761 | ||
![]() |
9caee357c0 | ||
![]() |
35d892c418 | ||
![]() |
9572a2a46b | ||
![]() |
8996361b26 | ||
![]() |
02ee731602 | ||
![]() |
bb1e6bf35b | ||
![]() |
c1b65285c1 | ||
![]() |
8b8d6e5fa3 | ||
![]() |
c34fe184e8 | ||
![]() |
7363838f86 | ||
![]() |
3081425ccd | ||
![]() |
95d494a54c | ||
![]() |
145e5d7bc6 | ||
![]() |
876fd9e85a | ||
![]() |
e8c30cabca | ||
![]() |
490f84a7b1 | ||
![]() |
ca28178b86 | ||
![]() |
2fceb0aeee | ||
![]() |
86f39d1d43 | ||
![]() |
1faf60444d | ||
![]() |
e927091d21 | ||
![]() |
cff2f856b3 | ||
![]() |
a743e3bbba | ||
![]() |
f8a52d250e | ||
![]() |
b70a523bdf | ||
![]() |
8f2ed747e6 | ||
![]() |
5deccefb15 | ||
![]() |
3f04abfa9d | ||
![]() |
8e55c83996 | ||
![]() |
dee59486ba | ||
![]() |
77ef509aea | ||
![]() |
bfa7bccfa6 | ||
![]() |
a8c365edc8 | ||
![]() |
94953ddf6c | ||
![]() |
6b67546daf | ||
![]() |
3e188d1f87 | ||
![]() |
f69eb15a90 | ||
![]() |
dfe348187f | ||
![]() |
9706c56c5c | ||
![]() |
3677c5be2c | ||
![]() |
bd339fa963 | ||
![]() |
28f1b6bdf4 | ||
![]() |
c5aac3b81d | ||
![]() |
70836597e9 | ||
![]() |
958a1de2fd | ||
![]() |
36d30266e3 | ||
![]() |
558ab9761d | ||
![]() |
269ef370e4 | ||
![]() |
ba2958ecd2 | ||
![]() |
3b8b6eb315 | ||
![]() |
4f13db3178 | ||
![]() |
ee7aa54ab4 | ||
![]() |
c305dd4cd5 | ||
![]() |
6865791596 | ||
![]() |
2099259393 | ||
![]() |
27ca45dc70 | ||
![]() |
d290c11219 | ||
![]() |
cabe10ffdb | ||
![]() |
aa562c21a8 | ||
![]() |
22175a7271 | ||
![]() |
1e0647c0d1 | ||
![]() |
58d94da8b3 | ||
![]() |
d97763a3e8 | ||
![]() |
aa129aa123 | ||
![]() |
f648317206 | ||
![]() |
0685fdf7c6 | ||
![]() |
6fd4cda534 |
@@ -3,10 +3,10 @@ const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const paths = require("./paths.js");
|
||||
const bundle = require("./bundle.js");
|
||||
const log = require("fancy-log");
|
||||
const WebpackBar = require("webpackbar");
|
||||
const paths = require("./paths.js");
|
||||
const bundle = require("./bundle.js");
|
||||
|
||||
class LogStartCompilePlugin {
|
||||
ignoredFirst = false;
|
||||
@@ -138,6 +138,8 @@ const createWebpackConfig = ({
|
||||
"lit/directives/cache$": "lit/directives/cache.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import type { Condition } from "../../../../src/data/automation";
|
||||
import type { ConditionWithShorthand } from "../../../../src/data/automation";
|
||||
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
||||
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
|
||||
@@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit
|
||||
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
|
||||
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
|
||||
|
||||
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
|
||||
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
||||
{
|
||||
name: "State",
|
||||
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
||||
@@ -69,6 +69,14 @@ const SCHEMAS: { name: string; conditions: Condition[] }[] = [
|
||||
name: "Trigger",
|
||||
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Shorthand",
|
||||
conditions: [
|
||||
{ and: HaLogicalCondition.defaultConfig.conditions },
|
||||
{ or: HaLogicalCondition.defaultConfig.conditions },
|
||||
{ not: HaLogicalCondition.defaultConfig.conditions },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-condition")
|
||||
|
@@ -159,13 +159,19 @@ export class DemoHaAlert extends LitElement {
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@@ -170,6 +170,7 @@ const SCHEMAS: {
|
||||
select: { options: ["Option 1", "Option 2"], mode: "list" },
|
||||
},
|
||||
},
|
||||
template: { name: "Template", selector: { template: {} } },
|
||||
select: {
|
||||
name: "Select",
|
||||
selector: {
|
||||
|
3
gallery/src/pages/components/ha-tip.markdown
Normal file
3
gallery/src/pages/components/ha-tip.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Tips
|
||||
---
|
73
gallery/src/pages/components/ha-tip.ts
Normal file
73
gallery/src/pages/components/ha-tip.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-tip";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
|
||||
const tips: (string | TemplateResult)[] = [
|
||||
"Test tip",
|
||||
"Bigger test tip, with some random text just to fill up as much space as possible without it looking like I'm really trying to to that",
|
||||
html`<i>Tip</i> <b>with</b> <sub>HTML</sub>`,
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-tip")
|
||||
export class DemoHaTip extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html` ${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-tip ${mode} demo">
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-tip {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-tip": DemoHaTip;
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-ansi-to-html";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
fetchHassioAddonLogs,
|
||||
@@ -11,7 +12,6 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addon-logs")
|
||||
@@ -40,9 +40,9 @@ class HassioAddonLogs extends LitElement {
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html
|
||||
? html`<ha-ansi-to-html
|
||||
.content=${this._content}
|
||||
></hassio-ansi-to-html>`
|
||||
></ha-ansi-to-html>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -166,7 +166,15 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("search")}
|
||||
@@ -182,7 +190,9 @@ export class HassioBackups extends LitElement {
|
||||
selectable
|
||||
hasFab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -10,6 +10,7 @@ import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
import "./hassio-update";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@@ -22,6 +23,31 @@ class HassioDashboard extends LitElement {
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
<a href="/hassio/store">
|
||||
<ha-fab
|
||||
.label=${this.supervisor.localize("panel.store")}
|
||||
extended
|
||||
class="non-tabs"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></a>
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -74,6 +100,12 @@ class HassioDashboard extends LitElement {
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-fab.non-tabs {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
@@ -73,6 +73,14 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
});
|
||||
});
|
||||
|
||||
// Forward keydown events to the main window for quickbar access
|
||||
document.body.addEventListener("keydown", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../src/panels/my/ha-panel-my";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
|
||||
const REDIRECTS: Redirects = {
|
||||
export const REDIRECTS: Redirects = {
|
||||
supervisor: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
|
@@ -8,24 +8,27 @@ import { atLeastVersion } from "../../src/common/config/version";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
atLeastVersion(hass.config.version, 2022, 5)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
|
@@ -23,6 +23,10 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/system-health/ha-config-system-health";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
@@ -30,11 +34,6 @@ import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const UNSUPPORTED_REASON_URL = {};
|
||||
const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -11,7 +12,6 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
@@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement {
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html .content=${this._content}>
|
||||
</hassio-ansi-to-html>`
|
||||
? html`<ha-ansi-to-html .content=${this._content}>
|
||||
</ha-ansi-to-html>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -106,9 +106,10 @@
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^7.0.1",
|
||||
"home-assistant-js-websocket": "^7.0.3",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@@ -15,7 +15,7 @@ if [ -z $(which hass) ]; then
|
||||
echo "Installing Home Asstant core from dev."
|
||||
python3 -m pip install --upgrade \
|
||||
colorlog \
|
||||
git+git://github.com/home-assistant/home-assistant.git@dev
|
||||
git+https://github.com/home-assistant/home-assistant.git@dev
|
||||
fi
|
||||
|
||||
if [ ! -d "${WD}/config" ]; then
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220405.0
|
||||
version = 20220427.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
16
src/common/datetime/duration.ts
Normal file
16
src/common/datetime/duration.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import secondsToDuration from "./seconds_to_duration";
|
||||
|
||||
const DAY_IN_SECONDS = 86400;
|
||||
const HOUR_IN_SECONDS = 3600;
|
||||
const MINUTE_IN_SECONDS = 60;
|
||||
|
||||
export const UNIT_TO_SECOND_CONVERT = {
|
||||
s: 1,
|
||||
min: MINUTE_IN_SECONDS,
|
||||
h: HOUR_IN_SECONDS,
|
||||
d: DAY_IN_SECONDS,
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: string, units: string): string =>
|
||||
secondsToDuration(parseFloat(duration) * UNIT_TO_SECOND_CONVERT[units]) ||
|
||||
"0";
|
@@ -12,7 +12,7 @@ export const isNavigationClick = (e: MouseEvent) => {
|
||||
|
||||
const anchor = e
|
||||
.composedPath()
|
||||
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
|
||||
.find((n) => (n as HTMLElement).tagName === "A") as
|
||||
| HTMLAnchorElement
|
||||
| undefined;
|
||||
if (
|
||||
|
@@ -13,6 +13,7 @@ import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -28,11 +29,27 @@ export const computeStateDisplay = (
|
||||
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericState(stateObj)) {
|
||||
// state is duration
|
||||
if (
|
||||
stateObj.attributes.device_class === "duration" &&
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
compareState,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
style: "currency",
|
||||
currency: stateObj.attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
|
@@ -1,244 +0,0 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
}
|
@@ -1,551 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { CharCode } from "./char-code";
|
||||
|
||||
const _debug = false;
|
||||
|
||||
export interface Match {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const _maxLen = 128;
|
||||
|
||||
function initTable() {
|
||||
const table: number[][] = [];
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
table.push(row.slice(0));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.codePointAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Underline:
|
||||
case CharCode.Dash:
|
||||
case CharCode.Period:
|
||||
case CharCode.Space:
|
||||
case CharCode.Slash:
|
||||
case CharCode.Backslash:
|
||||
case CharCode.SingleQuote:
|
||||
case CharCode.DoubleQuote:
|
||||
case CharCode.Colon:
|
||||
case CharCode.DollarSign:
|
||||
case CharCode.LessThan:
|
||||
case CharCode.OpenParen:
|
||||
case CharCode.OpenSquareBracket:
|
||||
return true;
|
||||
case undefined:
|
||||
return false;
|
||||
default:
|
||||
if (isEmojiImprecise(code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWhitespaceAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
||||
return word[pos] !== wordLow[pos];
|
||||
}
|
||||
|
||||
export function isPatternInWord(
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternLen: number,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
fillMinWordPosArr = false
|
||||
): boolean {
|
||||
while (patternPos < patternLen && wordPos < wordLen) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
if (fillMinWordPosArr) {
|
||||
// Remember the min word position for each pattern position
|
||||
_minWordMatchPos[patternPos] = wordPos;
|
||||
}
|
||||
patternPos += 1;
|
||||
}
|
||||
wordPos += 1;
|
||||
}
|
||||
return patternPos === patternLen; // pattern must be exhausted
|
||||
}
|
||||
|
||||
enum Arrow {
|
||||
Diag = 1,
|
||||
Left = 2,
|
||||
LeftLeft = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* An array representing a fuzzy match.
|
||||
*
|
||||
* 0. the score
|
||||
* 1. the offset at which matching started
|
||||
* 2. `<match_pos_N>`
|
||||
* 3. `<match_pos_1>`
|
||||
* 4. `<match_pos_0>` etc
|
||||
*/
|
||||
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
|
||||
export type FuzzyScore = Array<number>;
|
||||
|
||||
export function fuzzyScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordStart: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined {
|
||||
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
||||
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
||||
|
||||
if (
|
||||
patternStart >= patternLen ||
|
||||
wordStart >= wordLen ||
|
||||
patternLen - patternStart > wordLen - wordStart
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Run a simple check if the characters of pattern occur
|
||||
// (in order) at all in word. If that isn't the case we
|
||||
// stop because no match will be possible
|
||||
if (
|
||||
!isPatternInWord(
|
||||
patternLow,
|
||||
patternStart,
|
||||
patternLen,
|
||||
wordLow,
|
||||
wordStart,
|
||||
wordLen,
|
||||
true
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the max matching word position for each pattern position
|
||||
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
|
||||
_fillInMaxWordMatchPos(
|
||||
patternLen,
|
||||
wordLen,
|
||||
patternStart,
|
||||
wordStart,
|
||||
patternLow,
|
||||
wordLow
|
||||
);
|
||||
|
||||
let row: number;
|
||||
let column = 1;
|
||||
let patternPos: number;
|
||||
let wordPos: number;
|
||||
|
||||
const hasStrongFirstMatch = [false];
|
||||
|
||||
// There will be a match, fill in tables
|
||||
for (
|
||||
row = 1, patternPos = patternStart;
|
||||
patternPos < patternLen;
|
||||
row++, patternPos++
|
||||
) {
|
||||
// Reduce search space to possible matching word positions and to possible access from next row
|
||||
const minWordMatchPos = _minWordMatchPos[patternPos];
|
||||
const maxWordMatchPos = _maxWordMatchPos[patternPos];
|
||||
const nextMaxWordMatchPos =
|
||||
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
|
||||
|
||||
for (
|
||||
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
|
||||
wordPos < nextMaxWordMatchPos;
|
||||
column++, wordPos++
|
||||
) {
|
||||
let score = Number.MIN_SAFE_INTEGER;
|
||||
let canComeDiag = false;
|
||||
|
||||
if (wordPos <= maxWordMatchPos) {
|
||||
score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos,
|
||||
wordLen,
|
||||
wordStart,
|
||||
_diag[row - 1][column - 1] === 0,
|
||||
hasStrongFirstMatch
|
||||
);
|
||||
}
|
||||
|
||||
let diagScore = 0;
|
||||
if (score !== Number.MAX_SAFE_INTEGER) {
|
||||
canComeDiag = true;
|
||||
diagScore = score + _table[row - 1][column - 1];
|
||||
}
|
||||
|
||||
const canComeLeft = wordPos > minWordMatchPos;
|
||||
const leftScore = canComeLeft
|
||||
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
const canComeLeftLeft =
|
||||
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
|
||||
const leftLeftScore = canComeLeftLeft
|
||||
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
if (
|
||||
canComeLeftLeft &&
|
||||
(!canComeLeft || leftLeftScore >= leftScore) &&
|
||||
(!canComeDiag || leftLeftScore >= diagScore)
|
||||
) {
|
||||
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
|
||||
_table[row][column] = leftLeftScore;
|
||||
_arrows[row][column] = Arrow.LeftLeft;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
|
||||
// always prefer choosing left since that means a match is earlier in the word
|
||||
_table[row][column] = leftScore;
|
||||
_arrows[row][column] = Arrow.Left;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeDiag) {
|
||||
_table[row][column] = diagScore;
|
||||
_arrows[row][column] = Arrow.Diag;
|
||||
_diag[row][column] = _diag[row - 1][column - 1] + 1;
|
||||
} else {
|
||||
throw new Error(`not possible`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
printTables(pattern, patternStart, word, wordStart);
|
||||
}
|
||||
|
||||
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
row--;
|
||||
column--;
|
||||
|
||||
const result: FuzzyScore = [_table[row][column], wordStart];
|
||||
|
||||
let backwardsDiagLength = 0;
|
||||
let maxMatchColumn = 0;
|
||||
|
||||
while (row >= 1) {
|
||||
// Find the column where we go diagonally up
|
||||
let diagColumn = column;
|
||||
do {
|
||||
const arrow = _arrows[row][diagColumn];
|
||||
if (arrow === Arrow.LeftLeft) {
|
||||
diagColumn -= 2;
|
||||
} else if (arrow === Arrow.Left) {
|
||||
diagColumn -= 1;
|
||||
} else {
|
||||
// found the diagonal
|
||||
break;
|
||||
}
|
||||
} while (diagColumn >= 1);
|
||||
|
||||
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
|
||||
if (
|
||||
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
|
||||
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
|
||||
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
|
||||
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
|
||||
) {
|
||||
diagColumn = column;
|
||||
}
|
||||
|
||||
if (diagColumn === column) {
|
||||
// this is a contiguous match
|
||||
backwardsDiagLength++;
|
||||
} else {
|
||||
backwardsDiagLength = 1;
|
||||
}
|
||||
|
||||
if (!maxMatchColumn) {
|
||||
// remember the last matched column
|
||||
maxMatchColumn = diagColumn;
|
||||
}
|
||||
|
||||
row--;
|
||||
column = diagColumn - 1;
|
||||
result.push(column);
|
||||
}
|
||||
|
||||
if (wordLen === patternLen) {
|
||||
// the word matches the pattern with all characters!
|
||||
// giving the score a total match boost (to come up ahead other words)
|
||||
result[0] += 2;
|
||||
}
|
||||
|
||||
// Add 1 penalty for each skipped character in the word
|
||||
const skippedCharsCount = maxMatchColumn - patternLen;
|
||||
result[0] -= skippedCharsCount;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _doScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
wordStart: number,
|
||||
newMatchStart: boolean,
|
||||
outFirstMatchStrong: boolean[]
|
||||
): number {
|
||||
if (patternLow[patternPos] !== wordLow[wordPos]) {
|
||||
return Number.MIN_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
let score = 1;
|
||||
let isGapLocation = false;
|
||||
if (wordPos === patternPos - patternStart) {
|
||||
// common prefix: `foobar <-> foobaz`
|
||||
// ^^^^^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
} else if (
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) &&
|
||||
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
|
||||
) {
|
||||
// hitting upper-case: `foo <-> forOthers`
|
||||
// ^^ ^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
isGapLocation = true;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos) &&
|
||||
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
|
||||
) {
|
||||
// hitting a separator: `. <-> foo.bar`
|
||||
// ^
|
||||
score = 5;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1)
|
||||
) {
|
||||
// post separator: `foo <-> bar_foo`
|
||||
// ^^^
|
||||
score = 5;
|
||||
isGapLocation = true;
|
||||
}
|
||||
|
||||
if (score > 1 && patternPos === patternStart) {
|
||||
outFirstMatchStrong[0] = true;
|
||||
}
|
||||
|
||||
if (!isGapLocation) {
|
||||
isGapLocation =
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) ||
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1);
|
||||
}
|
||||
|
||||
//
|
||||
if (patternPos === patternStart) {
|
||||
// first character in pattern
|
||||
if (wordPos > wordStart) {
|
||||
// the first pattern character would match a word character that is not at the word start
|
||||
// so introduce a penalty to account for the gap preceding this match
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
} else if (newMatchStart) {
|
||||
// this would be the beginning of a new match (i.e. there would be a gap before this location)
|
||||
score += isGapLocation ? 2 : 0;
|
||||
} else {
|
||||
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
|
||||
score += isGapLocation ? 0 : 1;
|
||||
}
|
||||
|
||||
if (wordPos + 1 === wordLen) {
|
||||
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
|
||||
// so pretend there is a gap after the last character in the word to normalize things
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function printTable(
|
||||
table: number[][],
|
||||
pattern: string,
|
||||
patternLen: number,
|
||||
word: string,
|
||||
wordLen: number
|
||||
): string {
|
||||
function pad(s: string, n: number, _pad = " ") {
|
||||
while (s.length < n) {
|
||||
s = _pad + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let ret = ` | |${word
|
||||
.split("")
|
||||
.map((c) => pad(c, 3))
|
||||
.join("|")}\n`;
|
||||
|
||||
for (let i = 0; i <= patternLen; i++) {
|
||||
if (i === 0) {
|
||||
ret += " |";
|
||||
} else {
|
||||
ret += `${pattern[i - 1]}|`;
|
||||
}
|
||||
ret +=
|
||||
table[i]
|
||||
.slice(0, wordLen + 1)
|
||||
.map((n) => pad(n.toString(), 3))
|
||||
.join("|") + "\n";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function printTables(
|
||||
pattern: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordStart: number
|
||||
): void {
|
||||
pattern = pattern.substr(patternStart);
|
||||
word = word.substr(wordStart);
|
||||
console.log(printTable(_table, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
|
||||
}
|
||||
|
||||
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
|
||||
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
|
||||
const _diag = initTable(); // the length of a contiguous diagonal match
|
||||
const _table = initTable();
|
||||
const _arrows = <Arrow[][]>initTable();
|
||||
|
||||
function initArr(maxLen: number) {
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function _fillInMaxWordMatchPos(
|
||||
patternLen: number,
|
||||
wordLen: number,
|
||||
patternStart: number,
|
||||
wordStart: number,
|
||||
patternLow: string,
|
||||
wordLow: string
|
||||
) {
|
||||
let patternPos = patternLen - 1;
|
||||
let wordPos = wordLen - 1;
|
||||
while (patternPos >= patternStart && wordPos >= wordStart) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
_maxWordMatchPos[patternPos] = wordPos;
|
||||
patternPos--;
|
||||
}
|
||||
wordPos--;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FuzzyScorer {
|
||||
(
|
||||
pattern: string,
|
||||
lowPattern: string,
|
||||
patternPos: number,
|
||||
word: string,
|
||||
lowWord: string,
|
||||
wordPos: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined;
|
||||
}
|
||||
|
||||
export function createMatches(score: undefined | FuzzyScore): Match[] {
|
||||
if (typeof score === "undefined") {
|
||||
return [];
|
||||
}
|
||||
const res: Match[] = [];
|
||||
const wordPos = score[1];
|
||||
for (let i = score.length - 1; i > 1; i--) {
|
||||
const pos = score[i] + wordPos;
|
||||
const last = res[res.length - 1];
|
||||
if (last && last.end === pos) {
|
||||
last.end = pos + 1;
|
||||
} else {
|
||||
res.push({ start: pos, end: pos + 1 });
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast function (therefore imprecise) to check if code points are emojis.
|
||||
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
|
||||
*/
|
||||
export function isEmojiImprecise(x: number): boolean {
|
||||
return (
|
||||
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
|
||||
x === 8986 ||
|
||||
x === 8987 ||
|
||||
x === 9200 ||
|
||||
x === 9203 ||
|
||||
(x >= 9728 && x <= 10175) ||
|
||||
x === 11088 ||
|
||||
x === 11093 ||
|
||||
(x >= 127744 && x <= 128591) ||
|
||||
(x >= 128640 && x <= 128764) ||
|
||||
(x >= 128992 && x <= 129003) ||
|
||||
(x >= 129280 && x <= 129535) ||
|
||||
(x >= 129648 && x <= 129750)
|
||||
);
|
||||
}
|
@@ -1,52 +1,4 @@
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
* Determine whether a sequence of letters exists in another string,
|
||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||
*
|
||||
* @param {string} filter - Sequence of letters to check for
|
||||
* @param {ScorableTextItem} item - Item against whose strings will be checked
|
||||
*
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (
|
||||
filter: string,
|
||||
item: ScorableTextItem
|
||||
) => {
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
0,
|
||||
word,
|
||||
word.toLowerCase(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
||||
if (!scores) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter returns a 0 for a weak match.
|
||||
// But if .filter() sees a "0", it considers that a failed match and will remove it.
|
||||
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return topScore;
|
||||
};
|
||||
import fuzzysort from "fuzzysort";
|
||||
|
||||
/**
|
||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||
@@ -66,18 +18,48 @@ export interface ScorableTextItem {
|
||||
strings: string[];
|
||||
}
|
||||
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
export type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
filter: string,
|
||||
items: T[]
|
||||
) => T[];
|
||||
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
|
||||
items
|
||||
export function fuzzyMatcher(search: string | null): (string) => boolean {
|
||||
const scorer = fuzzyScorer(search);
|
||||
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
export function fuzzyScorer(
|
||||
search: string | null
|
||||
): (values: string[]) => number {
|
||||
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
|
||||
if (!searchTerms) {
|
||||
return () => 0;
|
||||
}
|
||||
return (values) =>
|
||||
searchTerms
|
||||
.map((term) => {
|
||||
const resultsForTerm = fuzzysort.go(term, values, {
|
||||
allowTypo: true,
|
||||
});
|
||||
if (resultsForTerm.length > 0) {
|
||||
return Math.max(...resultsForTerm.map((result) => result.score));
|
||||
}
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
})
|
||||
.reduce((partial, current) => partial + current, 0);
|
||||
}
|
||||
|
||||
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
const scorer = fuzzyScorer(filter);
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = fuzzySequentialMatch(filter, item);
|
||||
item.score = scorer(item.strings);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined)
|
||||
.filter((item) => item.score !== undefined && item.score > -100000)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
};
|
||||
|
||||
export const defaultFuzzyFilterSort = fuzzySortFilterSort;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
|
||||
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
||||
const timeout = new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(`Timed out in ${ms} ms.`);
|
||||
|
18
src/common/util/subscribe-polling.ts
Normal file
18
src/common/util/subscribe-polling.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export const subscribePollingCollection = (
|
||||
hass: HomeAssistant,
|
||||
updateData: (hass: HomeAssistant) => void,
|
||||
interval: number
|
||||
) => {
|
||||
let timeout;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
await updateData(hass);
|
||||
} finally {
|
||||
timeout = setTimeout(() => fetchData(), interval);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
return () => clearTimeout(timeout);
|
||||
};
|
@@ -1,165 +1,167 @@
|
||||
export const currencies = [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYN",
|
||||
"BYR",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STD",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VEF",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
export const createCurrencyListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYN",
|
||||
"BYR",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STD",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VEF",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
]) {
|
||||
for (const currency of currencies) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerHTML = currency;
|
||||
|
@@ -7,25 +7,26 @@ import type {
|
||||
SortableColumnContainer,
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = filter.toUpperCase();
|
||||
const matcher = fuzzyMatcher(filter);
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
matcher(
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
)
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@@ -198,9 +198,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.hass,
|
||||
deviceEntityLookup[device.id]
|
||||
),
|
||||
area: device.area_id
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
}));
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
|
@@ -15,6 +15,7 @@ import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
@@ -336,11 +337,18 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this.comboBox as any).filteredItems = this._states.filter(
|
||||
(entityState) =>
|
||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
||||
const filterString = ev.detail.value;
|
||||
|
||||
const sortableEntityStates = this._states.map((entityState) => ({
|
||||
strings: [entityState.entity_id, computeStateName(entityState)],
|
||||
entityState: entityState,
|
||||
}));
|
||||
const sortedEntityStates = defaultFuzzyFilterSort(
|
||||
filterString,
|
||||
sortableEntityStates
|
||||
);
|
||||
(this.comboBox as any).filteredItems = sortedEntityStates.map(
|
||||
(sortableItem) => sortableItem.entityState
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -2,12 +2,12 @@ import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-checkbox";
|
||||
import type { HaCheckbox } from "./ha-checkbox";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-switch";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = [
|
||||
{
|
||||
@@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="base"> Basic analytics </span>
|
||||
<span slot="description" data-for="base">
|
||||
This includes information about your system.
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) =>
|
||||
html`<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics?.preferences[preference.key]}
|
||||
.preference=${preference.key}
|
||||
name=${preference.key}
|
||||
>
|
||||
</ha-checkbox>
|
||||
${!baseEnabled
|
||||
? html`<paper-tooltip animation-delay="0" position="right">
|
||||
You need to enable basic analytics for this option to be
|
||||
available
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="heading" data-for=${preference.key}>
|
||||
${preference.title}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference.key}>
|
||||
${preference.description}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for=${preference.key}>
|
||||
${preference.title}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference.key}>
|
||||
${preference.description}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference.key]}
|
||||
.preference=${preference.key}
|
||||
name=${preference.key}
|
||||
>
|
||||
</ha-switch>
|
||||
${!baseEnabled
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0" position="right">
|
||||
You need to enable basic analytics for this option to be
|
||||
available
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)}
|
||||
<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
|
||||
<span slot="description" data-for="diagnostics">
|
||||
Share crash reports when unexpected errors occur.
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
@@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const preference = (checkbox as any).preference;
|
||||
private _handleRowClick(ev: Event) {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const preference = (target as any).preference;
|
||||
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
|
||||
|
||||
if (preferences[preference] === checkbox.checked) {
|
||||
if (preferences[preference] === target.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences[preference] = checkbox.checked;
|
||||
preferences[preference] = target.checked;
|
||||
|
||||
if (
|
||||
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
|
||||
checkbox.checked
|
||||
target.checked
|
||||
) {
|
||||
preferences.base = true;
|
||||
} else if (preference === "base" && !checkbox.checked) {
|
||||
} else if (preference === "base" && !target.checked) {
|
||||
preferences.usage = false;
|
||||
preferences.statistics = false;
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ interface State {
|
||||
backgroundColor: null | string;
|
||||
}
|
||||
|
||||
@customElement("hassio-ansi-to-html")
|
||||
class HassioAnsiToHtml extends LitElement {
|
||||
@customElement("ha-ansi-to-html")
|
||||
class HaAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
@@ -241,6 +241,6 @@ class HassioAnsiToHtml extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ansi-to-html": HassioAnsiToHtml;
|
||||
"ha-ansi-to-html": HaAnsiToHtml;
|
||||
}
|
||||
}
|
@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
name,
|
||||
});
|
||||
this._areas = [...this._areas!, area];
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
(this.comboBox as any).filteredItems = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
|
@@ -310,6 +310,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 40px;
|
||||
|
68
src/components/ha-clickable-list-item.ts
Normal file
68
src/components/ha-clickable-list-item.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { css, CSSResult, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
|
||||
@customElement("ha-clickable-list-item")
|
||||
export class HaClickableListItem extends ListItemBase {
|
||||
@property() public href?: string;
|
||||
|
||||
@property({ type: Boolean }) public disableHref = false;
|
||||
|
||||
// property used only in css
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@query("a") private _anchor!: HTMLAnchorElement;
|
||||
|
||||
public render() {
|
||||
const r = super.render();
|
||||
const href = this.href || "";
|
||||
|
||||
return html`${this.disableHref
|
||||
? html`<a aria-role="option">${r}</a>`
|
||||
: html`<a aria-role="option" href=${href}>${r}</a>`}`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
this._anchor.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
:host([rtl]) span {
|
||||
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--mdc-list-side-padding, 20px);
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-clickable-list-item": HaClickableListItem;
|
||||
}
|
||||
}
|
@@ -250,6 +250,18 @@ export class HaComboBox extends LitElement {
|
||||
top: -7px;
|
||||
right: 36px;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) .toggle-button {
|
||||
left: 12px;
|
||||
right: auto;
|
||||
top: -10px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
left: 36px;
|
||||
right: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -98,6 +98,10 @@ export class HaDialog extends DialogBase {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .dialog-actions {
|
||||
left: 0px !important;
|
||||
right: auto !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Fab } from "@material/mwc-fab";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css } from "lit";
|
||||
|
||||
@customElement("ha-fab")
|
||||
export class HaFab extends Fab {
|
||||
@@ -7,6 +8,17 @@ export class HaFab extends Fab {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
|
||||
static override styles = Fab.styles.concat([
|
||||
css`
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-fab--extended
|
||||
.mdc-fab__icon {
|
||||
margin-left: 12px !important;
|
||||
margin-right: calc(12px - 20px) !important;
|
||||
}
|
||||
`,
|
||||
]);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -176,10 +176,24 @@ export class HaFileUpload extends LitElement {
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-right: 0px;
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
left: initial;
|
||||
right: 16px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field--filled
|
||||
.mdc-floating-label {
|
||||
left: initial;
|
||||
right: 48px;
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
|
79
src/components/ha-metric.ts
Normal file
79
src/components/ha-metric.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { roundWithOneDecimal } from "../util/calculate";
|
||||
import "./ha-bar";
|
||||
import "./ha-settings-row";
|
||||
|
||||
@customElement("ha-metric")
|
||||
class HaMetric extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
@property({ type: String }) public heading!: string;
|
||||
|
||||
@property({ type: String }) public tooltip?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const roundedValue = roundWithOneDecimal(this.value);
|
||||
return html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading"> ${this.heading} </span>
|
||||
<div slot="description" .title=${this.tooltip ?? ""}>
|
||||
<span class="value"> ${roundedValue} % </span>
|
||||
<ha-bar
|
||||
class=${classMap({
|
||||
"target-warning": roundedValue > 50,
|
||||
"target-critical": roundedValue > 85,
|
||||
})}
|
||||
.value=${this.value}
|
||||
></ha-bar>
|
||||
</div>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-bar {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-ok-color,
|
||||
var(--success-color)
|
||||
);
|
||||
}
|
||||
.target-warning {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-warning-color,
|
||||
var(--warning-color)
|
||||
);
|
||||
}
|
||||
.target-critical {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-critical-color,
|
||||
var(--error-color)
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-metric": HaMetric;
|
||||
}
|
||||
}
|
97
src/components/ha-navigation-list.ts
Normal file
97
src/components/ha-navigation-list.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-clickable-list-item";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-navigation-list")
|
||||
class HaNavigationList extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public pages!: PageNavigation[];
|
||||
|
||||
@property({ type: Boolean }) public hasSecondary = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-list>
|
||||
${this.pages.map(
|
||||
(page) => html`
|
||||
<ha-clickable-list-item
|
||||
graphic="avatar"
|
||||
.twoline=${this.hasSecondary}
|
||||
.hasMeta=${!this.narrow}
|
||||
@click=${this._entryClicked}
|
||||
href=${page.path}
|
||||
>
|
||||
<div
|
||||
slot="graphic"
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
.style="background-color: ${page.iconColor || "undefined"}"
|
||||
>
|
||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
<span>${page.name}</span>
|
||||
${this.hasSecondary
|
||||
? html`<span slot="secondary">${page.description}</span>`
|
||||
: ""}
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="meta"></ha-icon-next>`
|
||||
: ""}
|
||||
</ha-clickable-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _entryClicked(ev) {
|
||||
ev.currentTarget.blur();
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: block;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding: 8px;
|
||||
}
|
||||
.icon-background {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
ha-clickable-list-item {
|
||||
cursor: pointer;
|
||||
font-size: var(--navigation-list-item-title-font-size);
|
||||
padding: var(--navigation-list-item-padding) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-navigation-list": HaNavigationList;
|
||||
}
|
||||
}
|
@@ -163,6 +163,9 @@ export class HaNetwork extends LitElement {
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-display: contents;
|
||||
--settings-row-prefix-display: contents;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
|
@@ -47,6 +47,10 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
right: 16px !important;
|
||||
left: initial !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -76,6 +76,13 @@ export class HaLocationSelector extends LitElement {
|
||||
const radius = ev.detail.radius;
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 400px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -107,6 +107,7 @@ export class HaNumberSelector extends LitElement {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
|
56
src/components/ha-selector/ha-selector-template.ts
Normal file
56
src/components/ha-selector/ha-selector-template.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-input-helper-text";
|
||||
|
||||
@customElement("ha-selector-template")
|
||||
export class HaTemplateSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
|
||||
: ""}
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.readOnly=${this.disabled}
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
@value-changed=${this._handleChange}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const value = ev.target.value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-template": HaTemplateSelector;
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ import "./ha-selector-number";
|
||||
import "./ha-selector-object";
|
||||
import "./ha-selector-select";
|
||||
import "./ha-selector-target";
|
||||
import "./ha-selector-template";
|
||||
import "./ha-selector-text";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-icon";
|
||||
|
@@ -472,6 +472,7 @@ export class HaServiceControl extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
|
@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
|
||||
display: contents;
|
||||
}
|
||||
:host(:not([narrow])) .content {
|
||||
display: flex;
|
||||
display: var(--settings-row-content-display, flex);
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
@@ -68,7 +68,7 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
display: contents;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
display: flex;
|
||||
|
@@ -1051,9 +1051,6 @@ class HaSidebar extends LitElement {
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
.configuration-badge {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
|
@@ -616,6 +616,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-chip__icon {
|
||||
margin-right: -14px !important;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -91,6 +91,19 @@ export class HaTextField extends TextFieldBase {
|
||||
.mdc-text-field {
|
||||
overflow: var(--text-field-overflow);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
right: 10px !important;
|
||||
left: initial !important;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label {
|
||||
max-width: calc(100% - 48px);
|
||||
right: 48px !important;
|
||||
left: initial !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
38
src/components/ha-tip.ts
Normal file
38
src/components/ha-tip.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mdiLightbulbOutline } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-tip")
|
||||
class HaTip extends LitElement {
|
||||
public render() {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
|
||||
<span class="prefix">Tip!</span>
|
||||
<span class="text"><slot></slot></span>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tip": HaTip;
|
||||
}
|
||||
}
|
@@ -19,9 +19,9 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
import "../ha-input-helper-text";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -297,7 +297,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
return css`
|
||||
ha-map {
|
||||
display: block;
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -151,6 +151,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
ha-media-player-browse {
|
||||
--media-browser-max-height: calc(100vh - 65px);
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
@@ -163,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
ha-media-player-browse {
|
||||
position: initial;
|
||||
--media-browser-max-height: 100vh - 137px;
|
||||
height: 100vh - 137px;
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||
import "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -16,16 +18,13 @@ import {
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
browseMediaPlayer,
|
||||
@@ -40,18 +39,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import type { HaCard } from "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-fab";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-browse-media-tts";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -101,8 +100,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@query(".content") private _content?: HTMLDivElement;
|
||||
|
||||
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
|
||||
|
||||
private _headerOffsetHeight = 0;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
@@ -148,326 +145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._currentItem) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const currentItem = this._currentItem;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
const children = currentItem.children || [];
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
? html` <div
|
||||
class="header ${classMap({
|
||||
"no-img": !currentItem.thumbnail,
|
||||
"no-dialog": !this.dialog,
|
||||
})}"
|
||||
@transitionend=${this._setHeaderHeight}
|
||||
>
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style=${styleMap({
|
||||
backgroundImage: currentItem.thumbnail
|
||||
? `url(${currentItem.thumbnail})`
|
||||
: "none",
|
||||
})}
|
||||
>
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</ha-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${subtitle
|
||||
? html` <h2 class="subtitle">${subtitle}</h2> `
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play &&
|
||||
(!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
<div
|
||||
class="content"
|
||||
@scroll=${this._scroll}
|
||||
@touchmove=${this._scroll}
|
||||
>
|
||||
${
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<div
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._childClicked}
|
||||
>
|
||||
<ha-card outlined>
|
||||
<div class="thumbnail">
|
||||
${child.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${["app", "directory"].includes(
|
||||
child.media_class
|
||||
)
|
||||
? "centered-image"
|
||||
: ""} image lazythumbnail"
|
||||
data-src=${child.thumbnail}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class ||
|
||||
child.media_class
|
||||
: child.media_class
|
||||
].icon}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
${child.can_play
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
can_expand: child.can_expand,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">
|
||||
${child.title}
|
||||
<paper-tooltip
|
||||
fitToVisibleBounds
|
||||
position="top"
|
||||
offset="4"
|
||||
>${child.title}</paper-tooltip
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
graphic: true,
|
||||
lazythumbnail:
|
||||
mediaClass.show_list_images === true,
|
||||
})}
|
||||
data-src=${ifDefined(
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? child.thumbnail
|
||||
: undefined
|
||||
)}
|
||||
slot="graphic"
|
||||
>
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
show:
|
||||
!mediaClass.show_list_images ||
|
||||
!child.thumbnail,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<span class="title">${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachResizeObserver();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -583,6 +260,19 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachResizeObserver();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -590,16 +280,368 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._animateHeaderHeight();
|
||||
} else if (changedProps.has("_currentItem")) {
|
||||
this._setHeaderHeight();
|
||||
this._attachIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
private _actionClicked(ev: MouseEvent): void {
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._currentItem) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const currentItem = this._currentItem;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
const children = currentItem.children || [];
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
const backgroundImage = currentItem.thumbnail
|
||||
? this._getSignedThumbnail(currentItem.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
? html`
|
||||
<div
|
||||
class="header ${classMap({
|
||||
"no-img": !currentItem.thumbnail,
|
||||
"no-dialog": !this.dialog,
|
||||
})}"
|
||||
@transitionend=${this._setHeaderHeight}
|
||||
>
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: ${until(
|
||||
backgroundImage,
|
||||
""
|
||||
)}"
|
||||
>
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</ha-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${subtitle
|
||||
? html` <h2 class="subtitle">${subtitle}</h2> `
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play &&
|
||||
(!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<div
|
||||
class="content"
|
||||
@scroll=${this._scroll}
|
||||
@touchmove=${this._scroll}
|
||||
>
|
||||
${
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const backgroundImage = child.thumbnail
|
||||
? this._getSignedThumbnail(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||
<ha-card outlined>
|
||||
<div class="thumbnail">
|
||||
${child.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${["app", "directory"].includes(child.media_class)
|
||||
? "centered-image"
|
||||
: ""} image"
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class || child.media_class
|
||||
: child.media_class
|
||||
].icon}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
${child.can_play
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
can_expand: child.can_expand,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">
|
||||
${child.title}
|
||||
<paper-tooltip fitToVisibleBounds position="top" offset="4"
|
||||
>${child.title}</paper-tooltip
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const currentItem = this._currentItem;
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||
|
||||
const backgroundImage =
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? this._getSignedThumbnail(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<mwc-list-item
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
graphic: true,
|
||||
thumbnail: mediaClass.show_list_images === true,
|
||||
})}
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
slot="graphic"
|
||||
>
|
||||
<ha-icon-button
|
||||
class="play ${classMap({
|
||||
show: !mediaClass.show_list_images || !child.thumbnail,
|
||||
})}"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<span class="title">${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`;
|
||||
};
|
||||
|
||||
private async _getSignedThumbnail(
|
||||
thumbnailUrl: string | undefined
|
||||
): Promise<string> {
|
||||
if (!thumbnailUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
private _actionClicked = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
|
||||
this._runAction(item);
|
||||
}
|
||||
};
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||||
@@ -615,7 +657,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _childClicked(ev: MouseEvent): Promise<void> {
|
||||
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||
const target = ev.currentTarget as any;
|
||||
const item: MediaPlayerItem = target.item;
|
||||
|
||||
@@ -631,7 +673,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [...this.navigateIds, item],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async _fetchData(
|
||||
entityId: string,
|
||||
@@ -658,55 +700,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load thumbnails for images on demand as they become visible.
|
||||
*/
|
||||
private async _attachIntersectionObserver(): Promise<void> {
|
||||
if (!("IntersectionObserver" in window) || !this._thumbnails) {
|
||||
return;
|
||||
}
|
||||
if (!this._intersectionObserver) {
|
||||
this._intersectionObserver = new IntersectionObserver(
|
||||
async (entries, observer) => {
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
const thumbnailCard = entry.target as HTMLElement;
|
||||
let thumbnailUrl = thumbnailCard.dataset.src;
|
||||
if (!thumbnailUrl) {
|
||||
return;
|
||||
}
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
|
||||
thumbnailUrl = signedPath.path;
|
||||
} else if (
|
||||
thumbnailUrl.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
|
||||
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
const observer = this._intersectionObserver!;
|
||||
for (const thumbnailCard of this._thumbnails) {
|
||||
observer.observe(thumbnailCard);
|
||||
}
|
||||
}
|
||||
|
||||
private _closeDialogAction(): void {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
@@ -841,6 +834,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
@@ -926,6 +920,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.not-shown {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
padding: 8px 16px 8px;
|
||||
}
|
||||
|
||||
.grid.not-shown {
|
||||
@@ -951,7 +946,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.children {
|
||||
mwc-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
@@ -988,7 +987,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
.portrait.children ha-card .thumbnail {
|
||||
.portrait ha-card .thumbnail {
|
||||
padding-bottom: 150%;
|
||||
}
|
||||
|
||||
@@ -1062,10 +1061,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover .lazythumbnail {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 16px;
|
||||
@@ -1127,7 +1122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .children {
|
||||
:host([narrow]) div.children {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
@@ -1232,6 +1227,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
--mdc-fab-box-shadow: none;
|
||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
overflow: overlay !important;
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
lit-virtualizer.not_shown {
|
||||
height: calc(100% - 36px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -152,6 +152,12 @@ export interface EventTrigger extends BaseTrigger {
|
||||
context?: ContextConstraint;
|
||||
}
|
||||
|
||||
export interface CalendarTrigger extends BaseTrigger {
|
||||
platform: "calendar";
|
||||
event: "start" | "end";
|
||||
entity_id: string;
|
||||
}
|
||||
|
||||
export type Trigger =
|
||||
| StateTrigger
|
||||
| MqttTrigger
|
||||
@@ -166,7 +172,8 @@ export type Trigger =
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger;
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@@ -226,6 +233,20 @@ export interface TriggerCondition extends BaseCondition {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
|
||||
|
||||
export interface ShorthandAndCondition extends ShorthandBaseCondition {
|
||||
and: Condition[];
|
||||
}
|
||||
|
||||
export interface ShorthandOrCondition extends ShorthandBaseCondition {
|
||||
or: Condition[];
|
||||
}
|
||||
|
||||
export interface ShorthandNotCondition extends ShorthandBaseCondition {
|
||||
not: Condition[];
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
@@ -237,6 +258,12 @@ export type Condition =
|
||||
| LogicalCondition
|
||||
| TriggerCondition;
|
||||
|
||||
export type ConditionWithShorthand =
|
||||
| Condition
|
||||
| ShorthandAndCondition
|
||||
| ShorthandOrCondition
|
||||
| ShorthandNotCondition;
|
||||
|
||||
export const triggerAutomationActions = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
|
@@ -1,4 +1,9 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface LogProvider {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<string>("GET", "error_log");
|
||||
|
@@ -1,278 +0,0 @@
|
||||
import {
|
||||
mdiEarth,
|
||||
mdiNavigationVariantOutline,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { domainIcon } from "../common/entity/domain_icon";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { ScorableTextItem } from "../common/string/filter/sequence-matching";
|
||||
import { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
import { computeDeviceName, DeviceRegistryEntry } from "./device_registry";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelNameTranslationKey } from "./panel";
|
||||
|
||||
export interface QuickBarItem extends ScorableTextItem {
|
||||
primaryText: string;
|
||||
primaryTextAlt?: string;
|
||||
secondaryText?: string;
|
||||
metaText?: string;
|
||||
categoryKey:
|
||||
| "reload"
|
||||
| "navigation"
|
||||
| "server_control"
|
||||
| "entity"
|
||||
| "suggestion";
|
||||
actionData: string | string[];
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
path?: string;
|
||||
isSuggestion?: boolean;
|
||||
}
|
||||
|
||||
export type NavigationInfo = PageNavigation &
|
||||
Pick<QuickBarItem, "primaryText" | "secondaryText">;
|
||||
|
||||
export type BaseNavigationCommand = Pick<QuickBarItem, "primaryText" | "path">;
|
||||
|
||||
export const generateEntityItems = (
|
||||
hass: HomeAssistant,
|
||||
entities: { [entityId: string]: EntityRegistryEntry },
|
||||
devices: { [deviceId: string]: DeviceRegistryEntry },
|
||||
areas: { [areaId: string]: AreaRegistryEntry }
|
||||
): QuickBarItem[] =>
|
||||
Object.keys(hass.states)
|
||||
.map((entityId) => {
|
||||
const entityState = hass.states[entityId];
|
||||
const entity = entities[entityId];
|
||||
const deviceName = entity?.device_id
|
||||
? computeDeviceName(devices[entity.device_id], hass)
|
||||
: undefined;
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(entityState),
|
||||
primaryTextAlt: computeStateDisplay(
|
||||
hass.localize,
|
||||
entityState,
|
||||
hass.locale
|
||||
),
|
||||
secondaryText:
|
||||
(deviceName ? `${deviceName} | ` : "") +
|
||||
(hass.userData?.showAdvanced ? entityId : ""),
|
||||
metaText: entity?.area_id ? areas[entity.area_id].name : undefined,
|
||||
icon: entityState.attributes.icon,
|
||||
iconPath: entityState.attributes.icon
|
||||
? undefined
|
||||
: domainIcon(computeDomain(entityId), entityState),
|
||||
actionData: entityId,
|
||||
categoryKey: "entity" as const,
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.secondaryText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.primaryText, b.primaryText));
|
||||
|
||||
export const generateCommandItems = (
|
||||
hass: HomeAssistant
|
||||
): Array<QuickBarItem[]> => [
|
||||
generateNavigationCommands(hass),
|
||||
generateReloadCommands(hass),
|
||||
generateServerControlCommands(hass),
|
||||
];
|
||||
|
||||
export const generateReloadCommands = (hass: HomeAssistant): QuickBarItem[] => {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primaryText:
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(hass.localize, domain)
|
||||
),
|
||||
actionData: [domain, "reload"],
|
||||
secondaryText: "Reload changes made to the domain file",
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
|
||||
actionData: ["frontend", "reload_themes"],
|
||||
secondaryText: "Reload changes made to themes.yaml",
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
|
||||
actionData: ["homeassistant", "reload_core_config"],
|
||||
secondaryText: "Reload changes made to configuration.yaml",
|
||||
});
|
||||
|
||||
return commands.map((command) => ({
|
||||
...command,
|
||||
categoryKey: "reload",
|
||||
iconPath: mdiReload,
|
||||
strings: [
|
||||
`${hass.localize("ui.dialogs.quick-bar.commands.types.reload")} ${
|
||||
command.primaryText
|
||||
}`,
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
export const generateServerControlCommands = (
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] => {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey: QuickBarItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.server_control.${action}`)
|
||||
),
|
||||
categoryKey,
|
||||
actionData: action,
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
strings: [
|
||||
`${hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
)} ${item.primaryText}`,
|
||||
],
|
||||
secondaryText: "Control your server",
|
||||
iconPath: mdiServerNetwork,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(hass);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
|
||||
return finalizeNavigationCommands([...panelItems, ...sectionItems], hass);
|
||||
};
|
||||
|
||||
export const generateNavigationPanelCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.keys(hass.panels)
|
||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||
.map((panelKey) => {
|
||||
const panel = hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
const primaryText =
|
||||
hass.localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon,
|
||||
secondaryText: "Panel",
|
||||
};
|
||||
});
|
||||
|
||||
export const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (!canShowPage(hass, page)) {
|
||||
continue;
|
||||
}
|
||||
if (!page.component) {
|
||||
continue;
|
||||
}
|
||||
const info = getNavigationInfoFromConfig(page, hass);
|
||||
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconPath: mdiNavigationVariantOutline,
|
||||
...info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const getNavigationInfoFromConfig = (
|
||||
page: PageNavigation,
|
||||
hass: HomeAssistant
|
||||
): NavigationInfo | undefined => {
|
||||
if (!page.component) {
|
||||
return undefined;
|
||||
}
|
||||
const caption = hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
if (page.translationKey && caption) {
|
||||
return {
|
||||
...page,
|
||||
primaryText: caption,
|
||||
secondaryText: "Configuration Page",
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const finalizeNavigationCommands = (
|
||||
items: BaseNavigationCommand[],
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] =>
|
||||
items.map((item) => {
|
||||
const categoryKey: QuickBarItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
secondaryText: "Navigation",
|
||||
iconPath: mdiEarth,
|
||||
...item,
|
||||
actionData: item.path!,
|
||||
};
|
||||
|
||||
return {
|
||||
categoryKey,
|
||||
...navItem,
|
||||
strings: [
|
||||
`${hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
)} ${navItem.primaryText}`,
|
||||
],
|
||||
};
|
||||
});
|
@@ -194,11 +194,24 @@ export interface ChooseAction {
|
||||
default?: Action | Action[];
|
||||
}
|
||||
|
||||
export interface IfAction {
|
||||
alias?: string;
|
||||
if: string | Condition[];
|
||||
then: Action | Action[];
|
||||
else?: Action | Action[];
|
||||
}
|
||||
|
||||
export interface VariablesAction {
|
||||
alias?: string;
|
||||
variables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface StopAction {
|
||||
alias?: string;
|
||||
stop: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
interface UnknownAction {
|
||||
alias?: string;
|
||||
[key: string]: unknown;
|
||||
@@ -215,8 +228,10 @@ export type Action =
|
||||
| WaitForTriggerAction
|
||||
| RepeatAction
|
||||
| ChooseAction
|
||||
| IfAction
|
||||
| VariablesAction
|
||||
| PlayMediaAction
|
||||
| StopAction
|
||||
| UnknownAction;
|
||||
|
||||
export interface ActionTypes {
|
||||
@@ -228,10 +243,12 @@ export interface ActionTypes {
|
||||
activate_scene: SceneAction;
|
||||
repeat: RepeatAction;
|
||||
choose: ChooseAction;
|
||||
if: IfAction;
|
||||
wait_for_trigger: WaitForTriggerAction;
|
||||
variables: VariablesAction;
|
||||
service: ServiceAction;
|
||||
play_media: PlayMediaAction;
|
||||
stop: StopAction;
|
||||
unknown: UnknownAction;
|
||||
}
|
||||
|
||||
@@ -299,12 +316,18 @@ export const getActionType = (action: Action): ActionType => {
|
||||
if ("choose" in action) {
|
||||
return "choose";
|
||||
}
|
||||
if ("if" in action) {
|
||||
return "if";
|
||||
}
|
||||
if ("wait_for_trigger" in action) {
|
||||
return "wait_for_trigger";
|
||||
}
|
||||
if ("variables" in action) {
|
||||
return "variables";
|
||||
}
|
||||
if ("stop" in action) {
|
||||
return "stop";
|
||||
}
|
||||
if ("service" in action) {
|
||||
if ("metadata" in action) {
|
||||
if (is(action, activateSceneActionStruct)) {
|
||||
|
@@ -19,6 +19,7 @@ export type Selector =
|
||||
| SelectSelector
|
||||
| StringSelector
|
||||
| TargetSelector
|
||||
| TemplateSelector
|
||||
| ThemeSelector
|
||||
| TimeSelector;
|
||||
|
||||
@@ -213,6 +214,11 @@ export interface TargetSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface TemplateSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
template: {};
|
||||
}
|
||||
|
||||
export interface ThemeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
theme: {};
|
||||
|
@@ -1,10 +1,15 @@
|
||||
import type {
|
||||
HassEntities,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
@@ -31,8 +36,12 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
|
||||
export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
||||
entity.state === BINARY_STATE_ON &&
|
||||
export const updateCanInstall = (
|
||||
entity: UpdateEntity,
|
||||
showSkipped = false
|
||||
): boolean =>
|
||||
(entity.state === BINARY_STATE_ON ||
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
@@ -43,3 +52,75 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
type: "update/release_notes",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
export const filterUpdateEntities = (entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
});
|
||||
|
||||
export const filterUpdateEntitiesWithInstall = (
|
||||
entities: HassEntities,
|
||||
showSkipped = false
|
||||
) =>
|
||||
filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
);
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
) => {
|
||||
const entities = filterUpdateEntities(hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
|
||||
if (!entities.length) {
|
||||
showAlertDialog(element, {
|
||||
title: hass.localize("ui.panel.config.updates.no_update_entities.title"),
|
||||
text: hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: entities,
|
||||
});
|
||||
|
||||
if (filterUpdateEntitiesWithInstall(hass.states).length) {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.updates_refreshed"),
|
||||
});
|
||||
} else {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -20,6 +20,7 @@ export const subscribeRenderTemplate = (
|
||||
entity_ids?: string | string[];
|
||||
variables?: Record<string, unknown>;
|
||||
timeout?: number;
|
||||
strict?: boolean;
|
||||
}
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
|
||||
|
@@ -127,7 +127,7 @@ export interface ZWaveJSClient {
|
||||
|
||||
export interface ZWaveJSController {
|
||||
home_id: number;
|
||||
library_version: string;
|
||||
sdk_version: string;
|
||||
type: number;
|
||||
own_node_id: number;
|
||||
is_secondary: boolean;
|
||||
@@ -136,7 +136,7 @@ export interface ZWaveJSController {
|
||||
was_real_primary: boolean;
|
||||
is_static_update_controller: boolean;
|
||||
is_slave: boolean;
|
||||
serial_api_version: string;
|
||||
firmware_version: string;
|
||||
manufacturer_id: number;
|
||||
product_id: number;
|
||||
product_type: number;
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
@@ -261,7 +260,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
>
|
||||
</ha-icon-button
|
||||
></a>
|
||||
@@ -273,7 +271,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${this._step === null
|
||||
@@ -521,7 +518,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.dialog-actions[rtl] {
|
||||
:host-context([style*="direction: rtl;"]) .dialog-actions {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
@@ -146,14 +146,14 @@ export const showOptionsFlowDialog = (
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.option.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.handler}.option.step.${step.step_id}.description`,
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
@@ -169,7 +169,7 @@ export const showOptionsFlowDialog = (
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
@@ -194,6 +194,10 @@ class StepFlowForm extends LitElement {
|
||||
word-break: break-word;
|
||||
padding-right: 72px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 72px !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -106,6 +106,10 @@ class StepFlowPickFlow extends LitElement {
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 66px !important;
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
div {
|
||||
max-height: calc(100vh - 134px);
|
||||
|
@@ -313,6 +313,10 @@ class StepFlowPickHandler extends LitElement {
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 66px !important;
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
mwc-list {
|
||||
max-height: calc(100vh - 134px);
|
||||
|
@@ -77,7 +77,7 @@ class DialogBox extends LitElement {
|
||||
dialogInitialFocus
|
||||
.value=${this._value || ""}
|
||||
@keyup=${this._handleKeyUp}
|
||||
@change=${this._valueChanged}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||
|
||||
@customElement("more-info-update")
|
||||
class MoreInfoUpdate extends LitElement {
|
||||
@@ -129,11 +130,20 @@ class MoreInfoUpdate extends LitElement {
|
||||
<div class="actions">
|
||||
${this.stateObj.attributes.auto_update
|
||||
? ""
|
||||
: this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
? html`
|
||||
<mwc-button @click=${this._handleClearSkipped}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === "off" ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -145,7 +155,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === "off" &&
|
||||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
@@ -207,6 +217,12 @@ class MoreInfoUpdate extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearSkipped(): void {
|
||||
this.hass.callService("update", "clear_skipped", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
hr {
|
||||
|
@@ -119,7 +119,15 @@ class MoreInfoVacuum extends LitElement {
|
||||
"ui.dialogs.more_info_control.vacuum.status"
|
||||
)}:
|
||||
</span>
|
||||
<span><strong>${stateObj.attributes.status}</strong></span>
|
||||
<span>
|
||||
<strong>
|
||||
${stateObj.attributes.status ||
|
||||
this.hass.localize(
|
||||
`component.vacuum.state._.${stateObj.state}`
|
||||
) ||
|
||||
stateObj.state}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
@@ -2,114 +2,118 @@ import "@lit-labs/virtualizer";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiConsoleLine,
|
||||
mdiEarth,
|
||||
mdiMagnify,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { canShowPage } from "../../common/config/can_show_page";
|
||||
import { componentsWithService } from "../../common/config/components_with_service";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
defaultFuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import "../../components/ha-chip";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-textfield";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../../panels/config/ha-panel-config";
|
||||
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import {
|
||||
generateCommandItems,
|
||||
generateEntityItems,
|
||||
QuickBarItem,
|
||||
} from "../../data/quick-bar";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
haStyleScrollbar,
|
||||
} from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
ConfirmationDialogParams,
|
||||
showConfirmationDialog,
|
||||
} from "../generic/show-dialog-box";
|
||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
primaryText: string;
|
||||
iconPath?: string;
|
||||
action(data?: any): void;
|
||||
}
|
||||
|
||||
interface CommandItem extends QuickBarItem {
|
||||
categoryKey: "reload" | "navigation" | "server_control";
|
||||
categoryText: string;
|
||||
}
|
||||
|
||||
interface EntityItem extends QuickBarItem {
|
||||
altText: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const isCommandItem = (item: QuickBarItem): item is CommandItem =>
|
||||
(item as CommandItem).categoryKey !== undefined;
|
||||
|
||||
interface QuickBarNavigationItem extends CommandItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
|
||||
|
||||
type BaseNavigationCommand = Pick<
|
||||
QuickBarNavigationItem,
|
||||
"primaryText" | "path"
|
||||
>;
|
||||
@customElement("ha-quick-bar")
|
||||
export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _items?: Array<QuickBarItem[]>;
|
||||
@state() private _commandItems?: CommandItem[];
|
||||
|
||||
private _filteredItems?: QuickBarItem[];
|
||||
@state() private _entityItems?: EntityItem[];
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _search = "";
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _done = false;
|
||||
@state() private _commandMode = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _hint?: string;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@query("ha-textfield", false) private _filterInputField?: HTMLElement;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("suggestions", true, {
|
||||
attribute: false,
|
||||
})
|
||||
private _suggestions: QuickBarItem[] = [];
|
||||
|
||||
private _focusSet = false;
|
||||
|
||||
private _focusListElement?: ListItem | null;
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
||||
this._hint = params.hint;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
this._initializeItemsIfNeeded();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
this._opened = false;
|
||||
this._focusSet = false;
|
||||
this._filter = "";
|
||||
@@ -117,46 +121,28 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("_entity") ||
|
||||
changedProperties.has("_areas") ||
|
||||
changedProperties.has("_devices")) &&
|
||||
this._areas &&
|
||||
this._devices &&
|
||||
this._entities
|
||||
) {
|
||||
this._initializeItems();
|
||||
this._opened = true;
|
||||
}
|
||||
}
|
||||
private _getItems = memoizeOne(
|
||||
(commandMode: boolean, commandItems, entityItems, filter: string) => {
|
||||
const items = commandMode ? commandItems : entityItems;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._opened) {
|
||||
if (items && filter && filter !== " ") {
|
||||
return this._filterItems(items, filter);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._open) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let sectionCount = 0;
|
||||
|
||||
if (this._items && this._filter && this._filter !== "") {
|
||||
const newFilteredItems: QuickBarItem[] = [];
|
||||
this._items.forEach((arr) => {
|
||||
const items = this._filterItems(arr, this._filter).slice(0, 3);
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sectionCount++;
|
||||
newFilteredItems.push(...items);
|
||||
});
|
||||
|
||||
this._filteredItems = newFilteredItems;
|
||||
} else {
|
||||
sectionCount++;
|
||||
this._filteredItems = this._suggestions;
|
||||
}
|
||||
const items: QuickBarItem[] | undefined = this._getItems(
|
||||
this._commandMode,
|
||||
this._commandItems,
|
||||
this._entityItems,
|
||||
this._filter
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -175,18 +161,28 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.value=${this._search}
|
||||
.icon=${true}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
icon
|
||||
.iconTrailing=${this._search !== undefined || this._narrow}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
${this._commandMode
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${this._search || this._narrow
|
||||
? html`
|
||||
<div slot="trailingIcon">
|
||||
@@ -209,11 +205,12 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
</ha-textfield>
|
||||
</div>
|
||||
${!this._filteredItems
|
||||
? html`
|
||||
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||
`
|
||||
: this._filteredItems.length === 0 && this._filter !== ""
|
||||
${!items
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: items.length === 0
|
||||
? html`
|
||||
<div class="nothing-found">
|
||||
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
|
||||
@@ -221,138 +218,129 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@click=${this._handleItemClick}
|
||||
class="ha-scrollbar"
|
||||
style=${styleMap({
|
||||
height: this._narrow
|
||||
? "calc(100vh - 56px)"
|
||||
: `${Math.min(
|
||||
this._filteredItems.length * 72 +
|
||||
sectionCount * 37 +
|
||||
18,
|
||||
this._done ? 600 : 0
|
||||
)}px`,
|
||||
})}
|
||||
.items=${this._filteredItems}
|
||||
.renderItem=${this._renderItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
${this._opened
|
||||
? html`<lit-virtualizer
|
||||
scroller
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@click=${this._handleItemClick}
|
||||
class="ha-scrollbar"
|
||||
style=${styleMap({
|
||||
height: this._narrow
|
||||
? "calc(100vh - 56px)"
|
||||
: `${Math.min(
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
500
|
||||
)}px`,
|
||||
})}
|
||||
.items=${items}
|
||||
.renderItem=${this._renderItem}
|
||||
>
|
||||
</lit-virtualizer>`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`}
|
||||
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
|
||||
${this._hint ? html`<ha-tip>${this._hint}</ha-tip>` : ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _initializeItemsIfNeeded() {
|
||||
if (this._commandMode) {
|
||||
this._commandItems = this._commandItems || this._generateCommandItems();
|
||||
} else {
|
||||
this._entityItems = this._entityItems || this._generateEntityItems();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private async _handleRangeChanged(e) {
|
||||
if (this._focusSet) {
|
||||
return;
|
||||
}
|
||||
if (e.firstVisible > -1) {
|
||||
this._focusSet = true;
|
||||
await this.updateComplete;
|
||||
this._setFocusFirstListItem();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderItem = (item: QuickBarItem, index: number): TemplateResult => {
|
||||
if (!item) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const previous = this._filteredItems![index - 1];
|
||||
|
||||
return html`
|
||||
<div class="entry-container" style="z-index: 5">
|
||||
${index === 0 || item?.categoryKey !== previous?.categoryKey
|
||||
? html`
|
||||
<div class="entry-title">
|
||||
${item.isSuggestion
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.suggestions"
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${item.categoryKey}`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.secondaryText)}
|
||||
.hasMeta=${Boolean(item.metaText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
class=${item.secondaryText ? "single-line" : ""}
|
||||
>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
|
||||
<span
|
||||
>${item.primaryText}
|
||||
<span class="secondary">${item.primaryTextAlt}</span></span
|
||||
>
|
||||
${item.secondaryText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.secondaryText}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${item.metaText
|
||||
? html`<ha-chip slot="meta">${item.metaText}</ha-chip>`
|
||||
: ""}
|
||||
</mwc-list-item>
|
||||
</div>
|
||||
`;
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
: this._renderEntityItem(item as EntityItem, index);
|
||||
};
|
||||
|
||||
private _initializeItems() {
|
||||
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||
for (const device of this._devices!) {
|
||||
deviceLookup[device.id] = device;
|
||||
}
|
||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
class="entity"
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon
|
||||
.icon=${item.icon}
|
||||
class="entity"
|
||||
slot="graphic"
|
||||
></ha-icon>`}
|
||||
<span>${item.primaryText}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
const entityLookup: { [entityId: string]: EntityRegistryEntry } = {};
|
||||
for (const entity of this._entities!) {
|
||||
entityLookup[entity.entity_id] = entity;
|
||||
}
|
||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
class="command-item"
|
||||
hasMeta
|
||||
>
|
||||
<span>
|
||||
<ha-chip
|
||||
.label=${item.categoryText}
|
||||
hasIcon
|
||||
class="command-category ${item.categoryKey}"
|
||||
>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="icon"
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
${item.categoryText}</ha-chip
|
||||
>
|
||||
</span>
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of this._areas!) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
this._items = this._items || [
|
||||
generateEntityItems(this.hass, entityLookup, deviceLookup, areaLookup),
|
||||
...generateCommandItems(this.hass),
|
||||
];
|
||||
<span class="command-text">${item.primaryText}</span>
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||
if (!this._suggestions.includes(item)) {
|
||||
this._suggestions.unshift({ ...item, isSuggestion: true });
|
||||
this._suggestions = this._suggestions.slice(0, 3);
|
||||
}
|
||||
this._addSpinnerToCommandItem(index);
|
||||
|
||||
this._addSpinnerToItem(index);
|
||||
|
||||
switch (item.categoryKey) {
|
||||
case "entity":
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: item.actionData as string,
|
||||
});
|
||||
break;
|
||||
case "reload":
|
||||
this.hass.callService(item.actionData[0], item.actionData[1]);
|
||||
break;
|
||||
case "navigation":
|
||||
navigate(item.actionData as string);
|
||||
break;
|
||||
case "server_control":
|
||||
showConfirmationDialog(this, {
|
||||
confirmText: this.hass.localize("ui.dialogs.generic.ok"),
|
||||
confirm: () =>
|
||||
this.hass.callService("homeassistant", item.actionData as string),
|
||||
});
|
||||
break;
|
||||
}
|
||||
await item.action();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -371,28 +359,56 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _getItemAtIndex(index: number): ListItem | null {
|
||||
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||
}
|
||||
|
||||
private _addSpinnerToCommandItem(index: number): void {
|
||||
const spinner = document.createElement("ha-circular-progress");
|
||||
spinner.size = "small";
|
||||
spinner.slot = "meta";
|
||||
spinner.active = true;
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
const newFilter = (ev.currentTarget as any).value;
|
||||
const oldCommandMode = this._commandMode;
|
||||
const oldSearch = this._search;
|
||||
let newCommandMode: boolean;
|
||||
let newSearch: string;
|
||||
|
||||
if (newFilter.startsWith(">")) {
|
||||
newCommandMode = true;
|
||||
newSearch = newFilter.substring(1);
|
||||
} else {
|
||||
newCommandMode = false;
|
||||
newSearch = newFilter;
|
||||
}
|
||||
|
||||
if (oldCommandMode === newCommandMode && oldSearch === newSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandMode = newCommandMode;
|
||||
this._search = newSearch;
|
||||
|
||||
if (this._hint) {
|
||||
this._hint = undefined;
|
||||
}
|
||||
|
||||
if (this._focusSet && this._focusListElement) {
|
||||
if (oldCommandMode !== this._commandMode) {
|
||||
this._focusSet = false;
|
||||
// @ts-ignore
|
||||
this._focusListElement.rippleHandlers.endFocus();
|
||||
this._initializeItemsIfNeeded();
|
||||
this._filter = this._search;
|
||||
} else {
|
||||
if (this._focusSet && this._focusListElement) {
|
||||
this._focusSet = false;
|
||||
// @ts-ignore
|
||||
this._focusListElement.rippleHandlers.endFocus();
|
||||
}
|
||||
this._debouncedSetFilter(this._search);
|
||||
}
|
||||
this._debouncedSetFilter(this._search);
|
||||
}
|
||||
|
||||
private _clearSearch() {
|
||||
@@ -438,62 +454,258 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const target =
|
||||
ev.target.nodeName === "MWC-LIST-ITEM"
|
||||
? ev.target
|
||||
: ev.target.parentElement === "MWC-LIST-ITEM"
|
||||
? ev.target.parentElement
|
||||
: ev.target.parentElement.parentElement;
|
||||
|
||||
// Need to resolve when clicking on the chip
|
||||
if (!target.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listItem = ev.target.closest("mwc-list-item");
|
||||
this.processItemAndCloseDialog(
|
||||
target.item,
|
||||
Number((target as HTMLElement).getAttribute("index"))
|
||||
listItem.item,
|
||||
Number(listItem.getAttribute("index"))
|
||||
);
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this.updateComplete.then(() => {
|
||||
this._done = true;
|
||||
private _generateEntityItems(): EntityItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => {
|
||||
const entityState = this.hass.states[entityId];
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(entityState),
|
||||
altText: entityId,
|
||||
icon: entityState.attributes.icon,
|
||||
iconPath: entityState.attributes.icon
|
||||
? undefined
|
||||
: domainIcon(computeDomain(entityId), entityState),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.altText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
|
||||
);
|
||||
}
|
||||
|
||||
private _generateCommandItems(): CommandItem[] {
|
||||
return [
|
||||
...this._generateReloadCommands(),
|
||||
...this._generateServerControlCommands(),
|
||||
...this._generateNavigationCommands(),
|
||||
].sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
|
||||
);
|
||||
}
|
||||
|
||||
private _generateReloadCommands(): CommandItem[] {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(this.hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primaryText:
|
||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
),
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
),
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.themes"
|
||||
),
|
||||
action: () => this.hass.callService("frontend", "reload_themes"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.reload"
|
||||
),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.core"
|
||||
),
|
||||
action: () =>
|
||||
this.hass.callService("homeassistant", "reload_core_config"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.reload"
|
||||
),
|
||||
});
|
||||
|
||||
return commands.map((command) => ({
|
||||
...command,
|
||||
categoryKey: "reload",
|
||||
strings: [`${command.categoryText} ${command.primaryText}`],
|
||||
}));
|
||||
}
|
||||
|
||||
private _generateServerControlCommands(): CommandItem[] {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
),
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
categoryKey,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
};
|
||||
|
||||
return this._generateConfirmationCommand(
|
||||
{
|
||||
...item,
|
||||
strings: [`${item.categoryText} ${item.primaryText}`],
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleRangeChanged(e) {
|
||||
if (this._focusSet) {
|
||||
return;
|
||||
}
|
||||
if (e.firstVisible > -1) {
|
||||
this._focusSet = true;
|
||||
await this.updateComplete;
|
||||
this._setFocusFirstListItem();
|
||||
}
|
||||
private _generateNavigationCommands(): CommandItem[] {
|
||||
const panelItems = this._generateNavigationPanelCommands();
|
||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
||||
|
||||
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
|
||||
}
|
||||
|
||||
private _getItemAtIndex(index: number): ListItem | null {
|
||||
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
|
||||
return Object.keys(this.hass.panels)
|
||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||
.map((panelKey) => {
|
||||
const panel = this.hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
const primaryText =
|
||||
this.hass.localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _addSpinnerToItem(index: number): void {
|
||||
const spinner = document.createElement("ha-circular-progress");
|
||||
spinner.size = "small";
|
||||
spinner.slot = "meta";
|
||||
spinner.active = true;
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (!canShowPage(this.hass, page)) {
|
||||
continue;
|
||||
}
|
||||
if (!page.component) {
|
||||
continue;
|
||||
}
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private _getNavigationInfoFromConfig(
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined {
|
||||
if (!page.component) {
|
||||
return undefined;
|
||||
}
|
||||
const caption = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
if (page.translationKey && caption) {
|
||||
return { ...page, primaryText: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _generateConfirmationCommand(
|
||||
item: CommandItem,
|
||||
confirmText: ConfirmationDialogParams["confirmText"]
|
||||
): CommandItem {
|
||||
return {
|
||||
...item,
|
||||
action: () =>
|
||||
showConfirmationDialog(this, {
|
||||
confirmText,
|
||||
confirm: item.action,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private _finalizeNavigationCommands(
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
...item,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(item.path),
|
||||
};
|
||||
|
||||
return {
|
||||
...navItem,
|
||||
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
|
||||
categoryKey,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleIfAlreadyOpened() {
|
||||
return this._opened ? !this._commandMode : false;
|
||||
}
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
haStyleDialog,
|
||||
haStyle,
|
||||
css`
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.heading ha-textfield {
|
||||
@@ -527,8 +739,8 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
mwc-list-item ha-icon,
|
||||
mwc-list-item ha-svg-icon {
|
||||
ha-icon.entity,
|
||||
ha-svg-icon.entity {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@@ -536,36 +748,42 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
--mdc-text-field-fill-color: transparent;
|
||||
--mdc-theme-primary: var(--divider-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textfield ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
.command-category {
|
||||
--ha-chip-icon-color: #585858;
|
||||
--ha-chip-text-color: #212121;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
padding-left: 16px;
|
||||
padding-top: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
.command-category.reload {
|
||||
--ha-chip-background-color: #cddc39;
|
||||
}
|
||||
|
||||
.command-category.navigation {
|
||||
--ha-chip-background-color: var(--light-primary-color);
|
||||
}
|
||||
|
||||
.command-category.server_control {
|
||||
--ha-chip-background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
span.command-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
mwc-list-item {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hint {
|
||||
mwc-list-item.command-item {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
ha-tip {
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nothing-found {
|
||||
@@ -581,10 +799,6 @@ export class QuickBar extends SubscribeMixin(LitElement) {
|
||||
lit-virtualizer {
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
ha-chip {
|
||||
margin-left: -48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -5,15 +5,22 @@ import { property, state } from "lit/decorators";
|
||||
class HaInitPage extends LitElement {
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@state() showProgressIndicator = false;
|
||||
@state() private _showProgressIndicator = false;
|
||||
|
||||
private _showProgressIndicatorTimeout;
|
||||
@state() private _retryInSeconds = 60;
|
||||
|
||||
private _showProgressIndicatorTimeout?: NodeJS.Timeout;
|
||||
|
||||
private _retryInterval?: NodeJS.Timeout;
|
||||
|
||||
protected render() {
|
||||
return this.error
|
||||
? html`
|
||||
<p>Unable to connect to Home Assistant.</p>
|
||||
<mwc-button @click=${this._retry}>Retry</mwc-button>
|
||||
<p class="retry-text">
|
||||
Retrying in ${this._retryInSeconds} seconds...
|
||||
</p>
|
||||
<mwc-button @click=${this._retry}>Retry now</mwc-button>
|
||||
${location.host.includes("ui.nabu.casa")
|
||||
? html`
|
||||
<p>
|
||||
@@ -29,7 +36,7 @@ class HaInitPage extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div id="progress-indicator-wrapper">
|
||||
${this.showProgressIndicator
|
||||
${this._showProgressIndicator
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -39,14 +46,26 @@ class HaInitPage extends LitElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearTimeout(this._showProgressIndicatorTimeout);
|
||||
if (this._showProgressIndicatorTimeout) {
|
||||
clearTimeout(this._showProgressIndicatorTimeout);
|
||||
}
|
||||
if (this._retryInterval) {
|
||||
clearInterval(this._retryInterval);
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._showProgressIndicatorTimeout = setTimeout(async () => {
|
||||
await import("../components/ha-circular-progress");
|
||||
this.showProgressIndicator = true;
|
||||
this._showProgressIndicator = true;
|
||||
}, 5000);
|
||||
|
||||
this._retryInterval = setInterval(() => {
|
||||
const remainingSeconds = this._retryInSeconds--;
|
||||
if (remainingSeconds <= 0) {
|
||||
this._retry();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private _retry() {
|
||||
@@ -70,6 +89,9 @@ class HaInitPage extends LitElement {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.retry-text {
|
||||
margin-top: 0;
|
||||
}
|
||||
p,
|
||||
#loading-text {
|
||||
max-width: 350px;
|
||||
|
@@ -99,6 +99,7 @@ class HassSubpage extends LitElement {
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
|
@@ -130,7 +130,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
* Array of tabs to show on the page.
|
||||
* @type {Array}
|
||||
*/
|
||||
@property() public tabs!: PageNavigation[];
|
||||
@property() public tabs: PageNavigation[] = [];
|
||||
|
||||
/**
|
||||
* Force hides the filter menu.
|
||||
@@ -283,6 +283,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -82,6 +82,16 @@ class HassTabsSubpage extends LitElement {
|
||||
(!page.advancedOnly || showAdvanced)
|
||||
);
|
||||
|
||||
if (shownTabs.length < 2) {
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
page.translationKey ? localizeFunc(page.translationKey) : page.name,
|
||||
];
|
||||
}
|
||||
return [""];
|
||||
}
|
||||
|
||||
return shownTabs.map(
|
||||
(page) =>
|
||||
html`
|
||||
@@ -134,7 +144,7 @@ class HassTabsSubpage extends LitElement {
|
||||
this.narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
const showTabs = tabs.length > 1 || !this.narrow;
|
||||
const showTabs = tabs.length > 1;
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
@@ -159,8 +169,10 @@ class HassTabsSubpage extends LitElement {
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow
|
||||
? html`<div class="main-title"><slot name="header"></slot></div>`
|
||||
${this.narrow || !showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: ""}
|
||||
${showTabs
|
||||
? html`
|
||||
@@ -283,6 +295,7 @@ class HassTabsSubpage extends LitElement {
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -86,7 +86,7 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
padding: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
hassio-ansi-to-html {
|
||||
ha-ansi-to-html {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
padding: 0 8px;
|
||||
|
@@ -202,7 +202,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configSections.areas}
|
||||
.route=${this.route}
|
||||
>
|
||||
${this.narrow
|
||||
|
@@ -82,7 +82,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configSections.areas}
|
||||
.route=${this.route}
|
||||
>
|
||||
<ha-icon-button
|
||||
|
@@ -32,9 +32,11 @@ import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-delay";
|
||||
import "./types/ha-automation-action-device_id";
|
||||
import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-if";
|
||||
import "./types/ha-automation-action-play_media";
|
||||
import "./types/ha-automation-action-repeat";
|
||||
import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
|
||||
@@ -49,7 +51,9 @@ const OPTIONS = [
|
||||
"wait_for_trigger",
|
||||
"repeat",
|
||||
"choose",
|
||||
"if",
|
||||
"device_id",
|
||||
"stop",
|
||||
];
|
||||
|
||||
const getType = (action: Action | undefined) => {
|
||||
@@ -410,7 +414,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
z-index: 3;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.rtl .card-menu {
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
right: initial;
|
||||
left: 16px;
|
||||
}
|
||||
|
@@ -0,0 +1,109 @@
|
||||
import { CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { Action, IfAction } from "../../../../../data/script";
|
||||
import { HaDeviceCondition } from "../../condition/types/ha-automation-condition-device";
|
||||
import { HaDeviceAction } from "./ha-automation-action-device_id";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { Condition } from "../../../../lovelace/common/validate-condition";
|
||||
import "../ha-automation-action";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-if")
|
||||
export class HaIfAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public action!: IfAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
if: [{ ...HaDeviceCondition.defaultConfig, condition: "device" }],
|
||||
then: [HaDeviceAction.defaultConfig],
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const action = this.action;
|
||||
|
||||
return html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.if.if"
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.conditions=${action.if}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._ifChanged}
|
||||
></ha-automation-condition>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.if.then"
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.actions=${action.then}
|
||||
@value-changed=${this._thenChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.if.else"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.actions=${action.else || []}
|
||||
@value-changed=${this._elseChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
||||
private _ifChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Condition[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
if: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _thenChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
then: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _elseChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
else: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-if": HaIfAction;
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { StopAction } from "../../../../../data/script";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-stop")
|
||||
export class HaStopAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public action!: StopAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { stop: "" };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { error, stop } = this.action;
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.stop.stop"
|
||||
)}
|
||||
.value=${stop}
|
||||
@change=${this._stopChanged}
|
||||
></ha-textfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.stop.error"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${error ?? false}
|
||||
@change=${this._errorChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _stopChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, stop: (ev.target as any).value },
|
||||
});
|
||||
}
|
||||
|
||||
private _errorChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, error: (ev.target as any).checked },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-stop": HaStopAction;
|
||||
}
|
||||
}
|
@@ -10,9 +10,7 @@ const SCHEMA: HaFormSchema[] = [
|
||||
{
|
||||
name: "wait_template",
|
||||
selector: {
|
||||
text: {
|
||||
multiline: true,
|
||||
},
|
||||
template: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@@ -188,7 +188,12 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
([key, value]) =>
|
||||
html`<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description">${value?.description}</span>
|
||||
<ha-markdown
|
||||
slot="description"
|
||||
class="card-content"
|
||||
breaks
|
||||
.content=${value?.description}
|
||||
></ha-markdown>
|
||||
${value?.selector
|
||||
? html`<ha-selector
|
||||
.hass=${this.hass}
|
||||
@@ -327,6 +332,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
`,
|
||||
|
@@ -245,7 +245,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.rtl .card-menu {
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
float: left;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import "../../../../../components/ha-textarea";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../../components/ha-textarea";
|
||||
import type { TemplateCondition } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { handleChangeEvent } from "../ha-automation-condition-row";
|
||||
@@ -18,28 +18,27 @@ export class HaTemplateCondition extends LitElement {
|
||||
protected render() {
|
||||
const { value_template } = this.condition;
|
||||
return html`
|
||||
<ha-textarea
|
||||
name="value_template"
|
||||
.label=${this.hass.localize(
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.template.value_template"
|
||||
)}
|
||||
*
|
||||
</p>
|
||||
<ha-code-editor
|
||||
.name=${"value_template"}
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${value_template}
|
||||
@input=${this._valueChanged}
|
||||
autocomplete-entities
|
||||
@value-changed=${this._valueChanged}
|
||||
dir="ltr"
|
||||
autogrow
|
||||
></ha-textarea>
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
handleChangeEvent(this, ev);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textarea {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -28,6 +28,7 @@ import {
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
@@ -44,6 +45,7 @@ import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
|
||||
const OPTIONS = [
|
||||
"calendar",
|
||||
"device",
|
||||
"event",
|
||||
"state",
|
||||
@@ -442,7 +444,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
z-index: 3;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.rtl .card-menu {
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
float: left;
|
||||
}
|
||||
.triggered {
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { CalendarTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import type { HaFormSchema } from "../../../../../components/ha-form/types";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
@customElement("ha-automation-trigger-calendar")
|
||||
export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: CalendarTrigger;
|
||||
|
||||
private _schema = memoizeOne((localize: LocalizeFunc) => [
|
||||
{
|
||||
name: "entity_id",
|
||||
required: true,
|
||||
selector: { entity: { domain: "calendar" } },
|
||||
},
|
||||
{
|
||||
name: "event",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: [
|
||||
[
|
||||
"start",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.start"
|
||||
),
|
||||
],
|
||||
[
|
||||
"end",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.end"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
event: "start" as CalendarTrigger["event"],
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema(this.hass.localize);
|
||||
return html`
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this.trigger}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newTrigger = ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: newTrigger });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-calendar": HaCalendarTrigger;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import "../../../../../components/ha-textarea";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { TemplateTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -18,28 +18,27 @@ export class HaTemplateTrigger extends LitElement {
|
||||
protected render() {
|
||||
const { value_template } = this.trigger;
|
||||
return html`
|
||||
<ha-textarea
|
||||
name="value_template"
|
||||
.label=${this.hass.localize(
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.template.value_template"
|
||||
)}
|
||||
*
|
||||
</p>
|
||||
<ha-code-editor
|
||||
.name=${"value_template"}
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${value_template}
|
||||
@input=${this._valueChanged}
|
||||
autocomplete-entities
|
||||
@value-changed=${this._valueChanged}
|
||||
dir="ltr"
|
||||
autogrow
|
||||
></ha-textarea>
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
handleChangeEvent(this, ev);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textarea {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -34,7 +34,6 @@ import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@customElement("ha-config-backup")
|
||||
class HaConfigBackup extends LitElement {
|
||||
@@ -81,6 +80,7 @@ class HaConfigBackup extends LitElement {
|
||||
actions: {
|
||||
title: "",
|
||||
width: "15%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, backup: BackupContent) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -127,14 +127,22 @@ class HaConfigBackup extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
path: `/config/backup`,
|
||||
},
|
||||
]}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.backup}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.backup.picker.search"
|
||||
)}
|
||||
>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
|
@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.blueprints}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="entity_id"
|
||||
|
@@ -38,7 +38,7 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card header="Analytics">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>
|
||||
@@ -58,9 +58,9 @@ class ConfigAnalytics extends LitElement {
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">${analyticsLearnMore(this.hass)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,10 @@ class ConfigAnalytics extends LitElement {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
];
|
||||
}
|
||||
|
@@ -1,342 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { createCurrencyListEl } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
|
||||
@customElement("ha-config-core-form")
|
||||
class ConfigCoreForm extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _elevation?: string;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.form.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
list="timezones"
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZoneValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevationValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystemValue === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystemValue === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}<br />
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
list="currencies"
|
||||
.disabled=${disabled}
|
||||
.value=${this._currencyValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const tzInput = this.shadowRoot!.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as PaperInputElement;
|
||||
tzInput.inputElement.appendChild(createTimezoneListEl());
|
||||
|
||||
const cInput = this.shadowRoot!.querySelector(
|
||||
"[name=currency]"
|
||||
) as PaperInputElement;
|
||||
cInput.inputElement.appendChild(createCurrencyListEl());
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
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 _currencyValue() {
|
||||
return this._currency !== undefined
|
||||
? this._currency
|
||||
: this.hass.config.currency;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined
|
||||
? this._elevation
|
||||
: this.hass.config.elevation;
|
||||
}
|
||||
|
||||
private get _timeZoneValue() {
|
||||
return this._timeZone !== undefined
|
||||
? this._timeZone
|
||||
: this.hass.config.time_zone;
|
||||
}
|
||||
|
||||
private get _unitSystemValue() {
|
||||
return this._unitSystem !== undefined
|
||||
? this._unitSystem
|
||||
: this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
const location = this._location || [
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
];
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
currency: this._currencyValue,
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-core-form": ConfigCoreForm;
|
||||
}
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
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 "../../../layouts/hass-tabs-subpage";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-section-core";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaConfigCore extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.border {
|
||||
margin: 32px auto 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
max-width: 1040px;
|
||||
}
|
||||
|
||||
.narrow .border {
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-tabs-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
route="[[route]]"
|
||||
back-path="/config"
|
||||
tabs="[[_computeTabs()]]"
|
||||
show-advanced="[[showAdvanced]]"
|
||||
>
|
||||
<div class$="[[computeClasses(isWide)]]">
|
||||
<ha-config-section-core
|
||||
is-wide="[[isWide]]"
|
||||
show-advanced="[[showAdvanced]]"
|
||||
hass="[[hass]]"
|
||||
></ha-config-section-core>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
showAdvanced: Boolean,
|
||||
route: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_computeTabs() {
|
||||
return configSections.general;
|
||||
}
|
||||
|
||||
computeClasses(isWide) {
|
||||
return isWide ? "content" : "content narrow";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-core", HaConfigCore);
|
@@ -1,97 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
|
||||
@customElement("ha-config-name-form")
|
||||
class ConfigNameForm extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _name!: ConfigUpdateValues["location_name"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._nameValue}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _nameValue() {
|
||||
return this._name !== undefined
|
||||
? this._name
|
||||
: this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this._name = target.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._nameValue,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert("FAIL");
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-name-form": ConfigNameForm;
|
||||
}
|
||||
}
|
48
src/panels/config/core/ha-config-section-analytics.ts
Normal file
48
src/panels/config/core/ha-config-section-analytics.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./ha-config-analytics";
|
||||
|
||||
@customElement("ha-config-section-analytics")
|
||||
class HaConfigSectionAnalytics extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-config-analytics {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-analytics": HaConfigSectionAnalytics;
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import "../ha-config-section";
|
||||
import "./ha-config-analytics";
|
||||
import "./ha-config-core-form";
|
||||
import "./ha-config-name-form";
|
||||
import "./ha-config-network";
|
||||
import "./ha-config-url-form";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.core.section.core.header')]]</span
|
||||
>
|
||||
<span slot="introduction"
|
||||
>[[localize('ui.panel.config.core.section.core.introduction')]]</span
|
||||
>
|
||||
|
||||
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
|
||||
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
|
||||
<ha-config-url-form hass="[[hass]]"></ha-config-url-form>
|
||||
<ha-config-network hass="[[hass]]"></ha-config-network>
|
||||
<ha-config-analytics hass="[[hass]]"></ha-config-analytics>
|
||||
</ha-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
validating: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
value: null,
|
||||
},
|
||||
|
||||
validateLog: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
showAdvanced: Boolean,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-section-core", HaConfigSectionCore);
|
361
src/panels/config/core/ha-config-section-general.ts
Normal file
361
src/panels/config/core/ha-config-section-general.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import timezones from "google-timezones-json";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import { currencies } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _name?: string;
|
||||
|
||||
@state() private _elevation?: number;
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._submitting || !canEdit;
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.core.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${Object.keys(timezones).map(
|
||||
(tz) =>
|
||||
html`<mwc-list-item value=${tz}
|
||||
>${timezones[tz]}</mwc-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystem === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${currencies.map(
|
||||
(currency) =>
|
||||
html`<mwc-list-item .value=${currency}
|
||||
>${currency}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-locations-editor
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
`
|
||||
: html`
|
||||
<ha-settings-row>
|
||||
<div slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location"
|
||||
)}
|
||||
</div>
|
||||
<div slot="description" class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location_description"
|
||||
)}
|
||||
</div>
|
||||
<mwc-button @click=${this._editLocation}
|
||||
>${this.hass.localize("ui.common.edit")}</mwc-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._updateEntry}>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._unitSystem =
|
||||
this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
this._currency = this.hass.config.currency;
|
||||
this._elevation = this.hass.config.elevation;
|
||||
this._timeZone = this.hass.config.time_zone;
|
||||
this._name = this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
this._location = ev.detail.location;
|
||||
}
|
||||
|
||||
private async _updateEntry(ev: CustomEvent) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
}
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
currency: this._currency,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system: this._unitSystem,
|
||||
time_zone: this._timeZone,
|
||||
location_name: this._name,
|
||||
});
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
button.actionError();
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
location?: [number, number]
|
||||
): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: location ? location[0] : lat,
|
||||
longitude: location ? location[1] : lng,
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
private _editLocation() {
|
||||
navigate("/config/zone");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.card-content > * {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-general": HaConfigSectionGeneral;
|
||||
}
|
||||
}
|
208
src/panels/config/core/ha-config-section-updates.ts
Normal file
208
src/panels/config/core/ha-config-section-updates.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
HassioSupervisorInfo,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
fetchHassioSupervisorInfo(this.hass).then((data) => {
|
||||
this._supervisorInfo = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.updates.check_updates"
|
||||
)}
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item id="skipped">
|
||||
${this._showSkipped
|
||||
? this.hass.localize("ui.panel.config.updates.hide_skipped")
|
||||
: this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</mwc-list-item>
|
||||
${this._supervisorInfo?.channel !== "dev"
|
||||
? html`
|
||||
<mwc-list-item id="beta">
|
||||
${this._supervisorInfo?.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
`
|
||||
: html`
|
||||
${this.hass.localize("ui.panel.config.updates.no_updates")}
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._showSkipped = !this._showSkipped;
|
||||
break;
|
||||
case 1:
|
||||
this._toggleBeta();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleBeta(): Promise<void> {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.join_beta_channel.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.join_beta_channel.warning")}
|
||||
<br />
|
||||
<b> ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} </b>
|
||||
<br /><br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.release_items")}
|
||||
<ul>
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
</ul>
|
||||
<br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.confirm")}`,
|
||||
confirmText: this.hass.localize("ui.panel.config.updates.join_beta"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this._supervisorInfo!.channel === "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkUpdates(): Promise<void> {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesWithInstall(entities, showSkipped)
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-updates": HaConfigSectionUpdates;
|
||||
}
|
||||
}
|
145
src/panels/config/core/ha-config-system-navigation.ts
Normal file
145
src/panels/config/core/ha-config-system-navigation.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@customElement("ha-config-system-navigation")
|
||||
class HaConfigSystemNavigation extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = configSections.general
|
||||
.filter((page) => canShowPage(this.hass, page))
|
||||
.map((page) => ({
|
||||
...page,
|
||||
name: page.translationKey
|
||||
? this.hass.localize(page.translationKey)
|
||||
: page.name,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.restart_homeassistant"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
<ha-card outlined>
|
||||
<ha-navigation-list
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
></ha-navigation-list>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.confirm_restart"
|
||||
),
|
||||
confirm: () => {
|
||||
this.hass.callService("homeassistant", "restart");
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host(:not([narrow])) ha-card {
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-config-section {
|
||||
margin: auto;
|
||||
margin-top: -32px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-card {
|
||||
border-radius: 0;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-config-section {
|
||||
margin-top: -42px;
|
||||
}
|
||||
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: 16px;
|
||||
--navigation-list-item-padding: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-system-navigation": HaConfigSystemNavigation;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user