mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-14 14:19:50 +00:00
Compare commits
123 Commits
20220405.0
...
limit-quic
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
511368da13 | ||
![]() |
76e1721c58 | ||
![]() |
bad5a389b5 | ||
![]() |
85d1f49763 | ||
![]() |
7723d47ac1 | ||
![]() |
30b130ca74 | ||
![]() |
a124ec0717 | ||
![]() |
323d98ecf7 | ||
![]() |
125a601ae3 | ||
![]() |
3c549c6b31 | ||
![]() |
9c1494c74d | ||
![]() |
e751abd775 | ||
![]() |
714f2447b7 | ||
![]() |
d900e40d04 | ||
![]() |
8b82383790 | ||
![]() |
5a2cc2646c | ||
![]() |
16a0902989 | ||
![]() |
8f67aa38af | ||
![]() |
34184cf2ab | ||
![]() |
611cd2818e | ||
![]() |
0a4e8fd5d0 | ||
![]() |
11f0361f48 | ||
![]() |
cfa048ea4e | ||
![]() |
bbca7b762b | ||
![]() |
1dba849567 | ||
![]() |
aff1ec10bf | ||
![]() |
351ec08a71 |
@@ -3,10 +3,10 @@ const webpack = require("webpack");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
const paths = require("./paths.js");
|
|
||||||
const bundle = require("./bundle.js");
|
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
const WebpackBar = require("webpackbar");
|
const WebpackBar = require("webpackbar");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
const bundle = require("./bundle.js");
|
||||||
|
|
||||||
class LogStartCompilePlugin {
|
class LogStartCompilePlugin {
|
||||||
ignoredFirst = false;
|
ignoredFirst = false;
|
||||||
@@ -138,6 +138,8 @@ const createWebpackConfig = ({
|
|||||||
"lit/directives/cache$": "lit/directives/cache.js",
|
"lit/directives/cache$": "lit/directives/cache.js",
|
||||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||||
|
"@lit-labs/virtualizer/layouts/grid":
|
||||||
|
"@lit-labs/virtualizer/layouts/grid.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
|||||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
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 "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||||
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
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";
|
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 { 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";
|
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",
|
name: "State",
|
||||||
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
||||||
@@ -69,6 +69,14 @@ const SCHEMAS: { name: string; conditions: Condition[] }[] = [
|
|||||||
name: "Trigger",
|
name: "Trigger",
|
||||||
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
|
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")
|
@customElement("demo-automation-editor-condition")
|
||||||
|
@@ -159,13 +159,19 @@ export class DemoHaAlert extends LitElement {
|
|||||||
|
|
||||||
firstUpdated(changedProps) {
|
firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
applyThemesOnElement(
|
||||||
|
this.shadowRoot!.querySelector(".dark"),
|
||||||
|
{
|
||||||
default_theme: "default",
|
default_theme: "default",
|
||||||
default_dark_theme: "default",
|
default_dark_theme: "default",
|
||||||
themes: {},
|
themes: {},
|
||||||
darkMode: true,
|
darkMode: true,
|
||||||
theme: "default",
|
theme: "default",
|
||||||
});
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@@ -170,6 +170,7 @@ const SCHEMAS: {
|
|||||||
select: { options: ["Option 1", "Option 2"], mode: "list" },
|
select: { options: ["Option 1", "Option 2"], mode: "list" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
template: { name: "Template", selector: { template: {} } },
|
||||||
select: {
|
select: {
|
||||||
name: "Select",
|
name: "Select",
|
||||||
selector: {
|
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;
|
||||||
|
}
|
||||||
|
}
|
@@ -133,6 +133,12 @@ const ENTITIES = [
|
|||||||
friendly_name: "Update with auto update",
|
friendly_name: "Update with auto update",
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
}),
|
}),
|
||||||
|
getEntity("update", "update20", "on", {
|
||||||
|
...base_attributes,
|
||||||
|
in_progress: true,
|
||||||
|
title: undefined,
|
||||||
|
friendly_name: "Installing without title",
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@customElement("demo-more-info-update")
|
@customElement("demo-more-info-update")
|
||||||
|
@@ -39,7 +39,14 @@ import type { HomeAssistant } from "../../../../src/types";
|
|||||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
|
||||||
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
|
const SUPPORTED_UI_TYPES = [
|
||||||
|
"string",
|
||||||
|
"select",
|
||||||
|
"boolean",
|
||||||
|
"integer",
|
||||||
|
"float",
|
||||||
|
"schema",
|
||||||
|
];
|
||||||
|
|
||||||
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||||
new Type("!secret", {
|
new Type("!secret", {
|
||||||
@@ -48,6 +55,8 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||||
|
|
||||||
@customElement("hassio-addon-config")
|
@customElement("hassio-addon-config")
|
||||||
class HassioAddonConfig extends LitElement {
|
class HassioAddonConfig extends LitElement {
|
||||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||||
@@ -75,16 +84,63 @@ class HassioAddonConfig extends LitElement {
|
|||||||
public computeLabel = (entry: HaFormSchema): string =>
|
public computeLabel = (entry: HaFormSchema): string =>
|
||||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||||
?.name ||
|
?.name ||
|
||||||
this.addon.translations.en?.configuration?.[entry.name].name ||
|
this.addon.translations.en?.configuration?.[entry.name]?.name ||
|
||||||
entry.name;
|
entry.name;
|
||||||
|
|
||||||
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
|
public computeHelper = (entry: HaFormSchema): string =>
|
||||||
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
|
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||||
|
?.description ||
|
||||||
|
this.addon.translations.en?.configuration?.[entry.name]?.description ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
private _convertSchema = memoizeOne(
|
||||||
|
// Convert supervisor schema to selectors
|
||||||
|
(schema: Record<string, any>): HaFormSchema[] =>
|
||||||
schema.map((entry) =>
|
schema.map((entry) =>
|
||||||
entry.type === "select"
|
entry.type === "select"
|
||||||
? {
|
? {
|
||||||
...entry,
|
name: entry.name,
|
||||||
options: entry.options.map((option) => [option, option]),
|
required: entry.required,
|
||||||
|
selector: { select: { options: entry.options } },
|
||||||
|
}
|
||||||
|
: entry.type === "string"
|
||||||
|
? entry.multiple
|
||||||
|
? {
|
||||||
|
name: entry.name,
|
||||||
|
required: entry.required,
|
||||||
|
selector: {
|
||||||
|
select: { options: [], multiple: true, custom_value: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: entry.name,
|
||||||
|
required: entry.required,
|
||||||
|
selector: {
|
||||||
|
text: {
|
||||||
|
type:
|
||||||
|
entry.format || MASKED_FIELDS.includes(entry.name)
|
||||||
|
? "password"
|
||||||
|
: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: entry.type === "boolean"
|
||||||
|
? {
|
||||||
|
name: entry.name,
|
||||||
|
required: entry.required,
|
||||||
|
selector: { boolean: {} },
|
||||||
|
}
|
||||||
|
: entry.type === "schema"
|
||||||
|
? {
|
||||||
|
name: entry.name,
|
||||||
|
required: entry.required,
|
||||||
|
selector: { object: {} },
|
||||||
|
}
|
||||||
|
: entry.type === "float" || entry.type === "integer"
|
||||||
|
? {
|
||||||
|
name: entry.name,
|
||||||
|
required: entry.required,
|
||||||
|
selector: { number: { mode: "box" } },
|
||||||
}
|
}
|
||||||
: entry
|
: entry
|
||||||
)
|
)
|
||||||
@@ -140,7 +196,8 @@ class HassioAddonConfig extends LitElement {
|
|||||||
.data=${this._options!}
|
.data=${this._options!}
|
||||||
@value-changed=${this._configChanged}
|
@value-changed=${this._configChanged}
|
||||||
.computeLabel=${this.computeLabel}
|
.computeLabel=${this.computeLabel}
|
||||||
.schema=${this._schema(
|
.computeHelper=${this.computeHelper}
|
||||||
|
.schema=${this._convertSchema(
|
||||||
this._showOptional
|
this._showOptional
|
||||||
? this.addon.schema!
|
? this.addon.schema!
|
||||||
: this._filteredShchema(
|
: this._filteredShchema(
|
||||||
@@ -197,8 +254,9 @@ class HassioAddonConfig extends LitElement {
|
|||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._canShowSchema = !this.addon.schema!.find(
|
this._canShowSchema = !this.addon.schema!.find(
|
||||||
|
(entry) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
|
!SUPPORTED_UI_TYPES.includes(entry.type)
|
||||||
);
|
);
|
||||||
this._yamlMode = !this._canShowSchema;
|
this._yamlMode = !this._canShowSchema;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -8,10 +7,13 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../../src/components/ha-alert";
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
|
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||||
import {
|
import {
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
@@ -24,16 +26,6 @@ import { HomeAssistant } from "../../../../src/types";
|
|||||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
|
||||||
interface NetworkItem {
|
|
||||||
description: string;
|
|
||||||
container: string;
|
|
||||||
host: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NetworkItemInput extends PaperInputElement {
|
|
||||||
container: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("hassio-addon-network")
|
@customElement("hassio-addon-network")
|
||||||
class HassioAddonNetwork extends LitElement {
|
class HassioAddonNetwork extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -42,9 +34,13 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||||
|
|
||||||
|
@state() private _showOptional = false;
|
||||||
|
|
||||||
|
@state() private _configHasChanged = false;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _config?: NetworkItem[];
|
@state() private _config?: Record<string, any>;
|
||||||
|
|
||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
@@ -56,6 +52,10 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasHiddenOptions = Object.keys(this._config).find(
|
||||||
|
(entry) => this._config![entry] === null
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card
|
<ha-card
|
||||||
.header=${this.supervisor.localize(
|
.header=${this.supervisor.localize(
|
||||||
@@ -63,52 +63,49 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
${this.supervisor.localize(
|
||||||
|
"addon.configuration.network.introduction"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<table>
|
<ha-form
|
||||||
<tbody>
|
.data=${this._config}
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
${this.supervisor.localize(
|
|
||||||
"addon.configuration.network.container"
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
${this.supervisor.localize(
|
|
||||||
"addon.configuration.network.host"
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th>${this.supervisor.localize("common.description")}</th>
|
|
||||||
</tr>
|
|
||||||
${this._config!.map(
|
|
||||||
(item) => html`
|
|
||||||
<tr>
|
|
||||||
<td>${item.container}</td>
|
|
||||||
<td>
|
|
||||||
<paper-input
|
|
||||||
@value-changed=${this._configChanged}
|
@value-changed=${this._configChanged}
|
||||||
placeholder=${this.supervisor.localize(
|
.computeLabel=${this._computeLabel}
|
||||||
"addon.configuration.network.disabled"
|
.computeHelper=${this._computeHelper}
|
||||||
|
.schema=${this._createSchema(
|
||||||
|
this._config,
|
||||||
|
this._showOptional,
|
||||||
|
this.hass.userData?.showAdvanced || false
|
||||||
)}
|
)}
|
||||||
.value=${item.host ? String(item.host) : ""}
|
></ha-form>
|
||||||
.container=${item.container}
|
|
||||||
no-label-float
|
|
||||||
></paper-input>
|
|
||||||
</td>
|
|
||||||
<td>${this._computeDescription(item)}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
${hasHiddenOptions
|
||||||
|
? html`<ha-formfield
|
||||||
|
class="show-optional"
|
||||||
|
.label=${this.supervisor.localize(
|
||||||
|
"addon.configuration.network.show_disabled"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._toggleOptional}
|
||||||
|
.checked=${this._showOptional}
|
||||||
|
>
|
||||||
|
</ha-switch>
|
||||||
|
</ha-formfield>`
|
||||||
|
: ""}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||||
${this.supervisor.localize("common.reset_defaults")}
|
${this.supervisor.localize("common.reset_defaults")}
|
||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
<ha-progress-button @click=${this._saveTapped}>
|
<ha-progress-button
|
||||||
|
@click=${this._saveTapped}
|
||||||
|
.disabled=${!this._configHasChanged}
|
||||||
|
>
|
||||||
${this.supervisor.localize("common.save")}
|
${this.supervisor.localize("common.save")}
|
||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,50 +120,60 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeDescription = (item: NetworkItem): string =>
|
private _createSchema = memoizeOne(
|
||||||
this.addon.translations[this.hass.language]?.network?.[item.container]
|
(
|
||||||
?.description ||
|
config: Record<string, number>,
|
||||||
this.addon.translations.en?.network?.[item.container]?.description ||
|
showOptional: boolean,
|
||||||
item.description;
|
advanced: boolean
|
||||||
|
): HaFormSchema[] =>
|
||||||
|
(showOptional
|
||||||
|
? Object.keys(config)
|
||||||
|
: Object.keys(config).filter((entry) => config[entry] !== null)
|
||||||
|
).map((entry) => ({
|
||||||
|
name: entry,
|
||||||
|
selector: {
|
||||||
|
number: {
|
||||||
|
mode: "box",
|
||||||
|
min: 0,
|
||||||
|
max: 65535,
|
||||||
|
unit_of_measurement: advanced ? entry : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
private _computeLabel = (_: HaFormSchema): string => "";
|
||||||
|
|
||||||
|
private _computeHelper = (item: HaFormSchema): string =>
|
||||||
|
this.addon.translations[this.hass.language]?.network?.[item.name] ||
|
||||||
|
this.addon.translations.en?.network?.[item.name] ||
|
||||||
|
this.addon.network_description?.[item.name] ||
|
||||||
|
item.name;
|
||||||
|
|
||||||
private _setNetworkConfig(): void {
|
private _setNetworkConfig(): void {
|
||||||
const network = this.addon.network || {};
|
this._config = this.addon.network || {};
|
||||||
const description = this.addon.network_description || {};
|
|
||||||
const items: NetworkItem[] = Object.keys(network).map((key) => ({
|
|
||||||
container: key,
|
|
||||||
host: network[key],
|
|
||||||
description: description[key],
|
|
||||||
}));
|
|
||||||
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _configChanged(ev: Event): Promise<void> {
|
private async _configChanged(ev: CustomEvent): Promise<void> {
|
||||||
const target = ev.target as NetworkItemInput;
|
this._configHasChanged = true;
|
||||||
this._config!.forEach((item) => {
|
this._config! = ev.detail.value;
|
||||||
if (
|
|
||||||
item.container === target.container &&
|
|
||||||
item.host !== parseInt(String(target.value), 10)
|
|
||||||
) {
|
|
||||||
item.host = target.value ? parseInt(String(target.value), 10) : null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
const button = ev.currentTarget as any;
|
||||||
button.progress = true;
|
|
||||||
|
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
network: null,
|
network: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
const eventdata = {
|
const eventdata = {
|
||||||
success: true,
|
success: true,
|
||||||
response: undefined,
|
response: undefined,
|
||||||
path: "option",
|
path: "option",
|
||||||
};
|
};
|
||||||
|
button.actionSuccess();
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
if (this.addon?.state === "started") {
|
if (this.addon?.state === "started") {
|
||||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||||
@@ -177,19 +184,21 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
"error",
|
"error",
|
||||||
extractApiErrorMessage(err)
|
extractApiErrorMessage(err)
|
||||||
);
|
);
|
||||||
|
button.actionError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.progress = false;
|
private _toggleOptional() {
|
||||||
|
this._showOptional = !this._showOptional;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
const button = ev.currentTarget as any;
|
||||||
button.progress = true;
|
|
||||||
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const networkconfiguration = {};
|
const networkconfiguration = {};
|
||||||
this._config!.forEach((item) => {
|
Object.entries(this._config!).forEach(([key, value]) => {
|
||||||
networkconfiguration[item.container] = parseInt(String(item.host), 10);
|
networkconfiguration[key] = value ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const data: HassioAddonSetOptionParams = {
|
const data: HassioAddonSetOptionParams = {
|
||||||
@@ -198,11 +207,13 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
const eventdata = {
|
const eventdata = {
|
||||||
success: true,
|
success: true,
|
||||||
response: undefined,
|
response: undefined,
|
||||||
path: "option",
|
path: "option",
|
||||||
};
|
};
|
||||||
|
button.actionSuccess();
|
||||||
fireEvent(this, "hass-api-called", eventdata);
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
if (this.addon?.state === "started") {
|
if (this.addon?.state === "started") {
|
||||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||||
@@ -213,8 +224,8 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
"error",
|
"error",
|
||||||
extractApiErrorMessage(err)
|
extractApiErrorMessage(err)
|
||||||
);
|
);
|
||||||
|
button.actionError();
|
||||||
}
|
}
|
||||||
button.progress = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -232,6 +243,9 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.show-optional {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import "@material/mwc-button";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../../../src/components/ha-alert";
|
import "../../../../src/components/ha-alert";
|
||||||
|
import "../../../../src/components/ha-ansi-to-html";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonLogs,
|
fetchHassioAddonLogs,
|
||||||
@@ -11,7 +12,6 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
|||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/hassio-ansi-to-html";
|
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
|
||||||
@customElement("hassio-addon-logs")
|
@customElement("hassio-addon-logs")
|
||||||
@@ -40,9 +40,9 @@ class HassioAddonLogs extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this._content
|
${this._content
|
||||||
? html`<hassio-ansi-to-html
|
? html`<ha-ansi-to-html
|
||||||
.content=${this._content}
|
.content=${this._content}
|
||||||
></hassio-ansi-to-html>`
|
></ha-ansi-to-html>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { ActionDetail } from "@material/mwc-list";
|
import { ActionDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -166,7 +166,15 @@ export class HassioBackups extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<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}
|
.hass=${this.hass}
|
||||||
.localizeFunc=${this.supervisor.localize}
|
.localizeFunc=${this.supervisor.localize}
|
||||||
.searchLabel=${this.supervisor.localize("search")}
|
.searchLabel=${this.supervisor.localize("search")}
|
||||||
@@ -182,7 +190,9 @@ export class HassioBackups extends LitElement {
|
|||||||
selectable
|
selectable
|
||||||
hasFab
|
hasFab
|
||||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||||
back-path="/config"
|
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||||
|
? "/config/system"
|
||||||
|
: "/config"}
|
||||||
supervisor
|
supervisor
|
||||||
>
|
>
|
||||||
<ha-button-menu
|
<ha-button-menu
|
||||||
|
@@ -10,6 +10,7 @@ import { HomeAssistant, Route } from "../../../src/types";
|
|||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
import "./hassio-addons";
|
import "./hassio-addons";
|
||||||
import "./hassio-update";
|
import "./hassio-update";
|
||||||
|
import "../../../src/layouts/hass-subpage";
|
||||||
|
|
||||||
@customElement("hassio-dashboard")
|
@customElement("hassio-dashboard")
|
||||||
class HassioDashboard extends LitElement {
|
class HassioDashboard extends LitElement {
|
||||||
@@ -22,6 +23,31 @@ class HassioDashboard extends LitElement {
|
|||||||
@property({ attribute: false }) public route!: Route;
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
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`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -74,6 +100,12 @@ class HassioDashboard extends LitElement {
|
|||||||
.content {
|
.content {
|
||||||
margin: 0 auto;
|
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 { atLeastVersion } from "../../src/common/config/version";
|
||||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
|
||||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
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 { navigate } from "../../src/common/navigate";
|
||||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||||
@@ -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!);
|
makeDialogManager(this, this.shadowRoot!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "../../src/panels/my/ha-panel-my";
|
} from "../../src/panels/my/ha-panel-my";
|
||||||
import { HomeAssistant, Route } from "../../src/types";
|
import { HomeAssistant, Route } from "../../src/types";
|
||||||
|
|
||||||
const REDIRECTS: Redirects = {
|
export const REDIRECTS: Redirects = {
|
||||||
supervisor: {
|
supervisor: {
|
||||||
redirect: "/hassio/dashboard",
|
redirect: "/hassio/dashboard",
|
||||||
},
|
},
|
||||||
|
@@ -8,7 +8,10 @@ import { atLeastVersion } from "../../src/common/config/version";
|
|||||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||||
import { HomeAssistant } from "../../src/types";
|
import { HomeAssistant } from "../../src/types";
|
||||||
|
|
||||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
|
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||||
|
atLeastVersion(hass.config.version, 2022, 5)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
{
|
{
|
||||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||||
? "panel.addons"
|
? "panel.addons"
|
||||||
|
@@ -23,6 +23,10 @@ import {
|
|||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
} 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 { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||||
@@ -30,11 +34,6 @@ import { documentationUrl } from "../../../src/util/documentation-url";
|
|||||||
import "../components/supervisor-metric";
|
import "../components/supervisor-metric";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
const UNSUPPORTED_REASON_URL = {};
|
|
||||||
const UNHEALTHY_REASON_URL = {
|
|
||||||
privileged: "/more-info/unsupported/privileged",
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("hassio-supervisor-info")
|
@customElement("hassio-supervisor-info")
|
||||||
class HassioSupervisorInfo extends LitElement {
|
class HassioSupervisorInfo extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import "../../../src/components/ha-ansi-to-html";
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@@ -11,7 +12,6 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
|||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import "../components/hassio-ansi-to-html";
|
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
interface LogProvider {
|
interface LogProvider {
|
||||||
@@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
|
|
||||||
<div class="card-content" id="content">
|
<div class="card-content" id="content">
|
||||||
${this._content
|
${this._content
|
||||||
? html`<hassio-ansi-to-html .content=${this._content}>
|
? html`<ha-ansi-to-html .content=${this._content}>
|
||||||
</hassio-ansi-to-html>`
|
</ha-ansi-to-html>`
|
||||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
@@ -106,9 +106,10 @@
|
|||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
"deep-freeze": "^0.0.1",
|
"deep-freeze": "^0.0.1",
|
||||||
"fuse.js": "^6.0.0",
|
"fuse.js": "^6.0.0",
|
||||||
|
"fuzzysort": "^1.2.1",
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"hls.js": "^1.1.5",
|
"hls.js": "^1.1.5",
|
||||||
"home-assistant-js-websocket": "^7.0.1",
|
"home-assistant-js-websocket": "^7.0.3",
|
||||||
"idb-keyval": "^5.1.3",
|
"idb-keyval": "^5.1.3",
|
||||||
"intl-messageformat": "^9.9.1",
|
"intl-messageformat": "^9.9.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
@@ -15,7 +15,7 @@ if [ -z $(which hass) ]; then
|
|||||||
echo "Installing Home Asstant core from dev."
|
echo "Installing Home Asstant core from dev."
|
||||||
python3 -m pip install --upgrade \
|
python3 -m pip install --upgrade \
|
||||||
colorlog \
|
colorlog \
|
||||||
git+git://github.com/home-assistant/home-assistant.git@dev
|
git+https://github.com/home-assistant/home-assistant.git@dev
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d "${WD}/config" ]; then
|
if [ ! -d "${WD}/config" ]; then
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = home-assistant-frontend
|
name = home-assistant-frontend
|
||||||
version = 20220405.0
|
version = 20220427.0
|
||||||
author = The Home Assistant Authors
|
author = The Home Assistant Authors
|
||||||
author_email = hello@home-assistant.io
|
author_email = hello@home-assistant.io
|
||||||
license = Apache-2.0
|
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
|
const anchor = e
|
||||||
.composedPath()
|
.composedPath()
|
||||||
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
|
.find((n) => (n as HTMLElement).tagName === "A") as
|
||||||
| HTMLAnchorElement
|
| HTMLAnchorElement
|
||||||
| undefined;
|
| undefined;
|
||||||
if (
|
if (
|
||||||
|
@@ -29,8 +29,11 @@ import {
|
|||||||
mdiPowerPlug,
|
mdiPowerPlug,
|
||||||
mdiPowerPlugOff,
|
mdiPowerPlugOff,
|
||||||
mdiRadioboxBlank,
|
mdiRadioboxBlank,
|
||||||
mdiSmoke,
|
|
||||||
mdiSnowflake,
|
mdiSnowflake,
|
||||||
|
mdiSmokeDetector,
|
||||||
|
mdiSmokeDetectorAlert,
|
||||||
|
mdiSmokeDetectorVariant,
|
||||||
|
mdiSmokeDetectorVariantAlert,
|
||||||
mdiSquare,
|
mdiSquare,
|
||||||
mdiSquareOutline,
|
mdiSquareOutline,
|
||||||
mdiStop,
|
mdiStop,
|
||||||
@@ -52,6 +55,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
|||||||
return is_off ? mdiBattery : mdiBatteryOutline;
|
return is_off ? mdiBattery : mdiBatteryOutline;
|
||||||
case "battery_charging":
|
case "battery_charging":
|
||||||
return is_off ? mdiBattery : mdiBatteryCharging;
|
return is_off ? mdiBattery : mdiBatteryCharging;
|
||||||
|
case "carbon_monoxide":
|
||||||
|
return is_off ? mdiSmokeDetector : mdiSmokeDetectorAlert;
|
||||||
case "cold":
|
case "cold":
|
||||||
return is_off ? mdiThermometer : mdiSnowflake;
|
return is_off ? mdiThermometer : mdiSnowflake;
|
||||||
case "connectivity":
|
case "connectivity":
|
||||||
@@ -68,7 +73,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
|||||||
case "tamper":
|
case "tamper":
|
||||||
return is_off ? mdiCheckCircle : mdiAlertCircle;
|
return is_off ? mdiCheckCircle : mdiAlertCircle;
|
||||||
case "smoke":
|
case "smoke":
|
||||||
return is_off ? mdiCheckCircle : mdiSmoke;
|
return is_off ? mdiSmokeDetectorVariant : mdiSmokeDetectorVariantAlert;
|
||||||
case "heat":
|
case "heat":
|
||||||
return is_off ? mdiThermometer : mdiFire;
|
return is_off ? mdiThermometer : mdiFire;
|
||||||
case "light":
|
case "light":
|
||||||
|
@@ -13,6 +13,7 @@ import { formatNumber, isNumericState } from "../number/format_number";
|
|||||||
import { LocalizeFunc } from "../translations/localize";
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
import { supportsFeature } from "./supports-feature";
|
import { supportsFeature } from "./supports-feature";
|
||||||
|
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||||
|
|
||||||
export const computeStateDisplay = (
|
export const computeStateDisplay = (
|
||||||
localize: LocalizeFunc,
|
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`
|
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||||
if (isNumericState(stateObj)) {
|
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") {
|
if (stateObj.attributes.device_class === "monetary") {
|
||||||
try {
|
try {
|
||||||
return formatNumber(compareState, locale, {
|
return formatNumber(compareState, locale, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: stateObj.attributes.unit_of_measurement,
|
currency: stateObj.attributes.unit_of_measurement,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// fallback to default
|
// fallback to default
|
||||||
|
@@ -8,26 +8,25 @@ import {
|
|||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
mdiCast,
|
mdiCast,
|
||||||
mdiCastConnected,
|
mdiCastConnected,
|
||||||
|
mdiCheckCircleOutline,
|
||||||
mdiClock,
|
mdiClock,
|
||||||
|
mdiCloseCircleOutline,
|
||||||
mdiGestureTapButton,
|
mdiGestureTapButton,
|
||||||
mdiLanConnect,
|
mdiLanConnect,
|
||||||
mdiLanDisconnect,
|
mdiLanDisconnect,
|
||||||
mdiLightSwitch,
|
|
||||||
mdiLock,
|
mdiLock,
|
||||||
mdiLockAlert,
|
mdiLockAlert,
|
||||||
mdiLockClock,
|
mdiLockClock,
|
||||||
mdiLockOpen,
|
mdiLockOpen,
|
||||||
|
mdiPackage,
|
||||||
|
mdiPackageDown,
|
||||||
mdiPackageUp,
|
mdiPackageUp,
|
||||||
mdiPowerPlug,
|
mdiPowerPlug,
|
||||||
mdiPowerPlugOff,
|
mdiPowerPlugOff,
|
||||||
mdiRestart,
|
mdiRestart,
|
||||||
mdiToggleSwitch,
|
mdiToggleSwitchVariant,
|
||||||
mdiToggleSwitchOff,
|
mdiToggleSwitchVariantOff,
|
||||||
mdiCheckCircleOutline,
|
|
||||||
mdiCloseCircleOutline,
|
|
||||||
mdiWeatherNight,
|
mdiWeatherNight,
|
||||||
mdiPackage,
|
|
||||||
mdiPackageDown,
|
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
||||||
@@ -109,9 +108,11 @@ export const domainIcon = (
|
|||||||
case "outlet":
|
case "outlet":
|
||||||
return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
|
return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
|
||||||
case "switch":
|
case "switch":
|
||||||
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
|
return compareState === "on"
|
||||||
|
? mdiToggleSwitchVariant
|
||||||
|
: mdiToggleSwitchVariantOff;
|
||||||
default:
|
default:
|
||||||
return mdiLightSwitch;
|
return mdiToggleSwitchVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "sensor": {
|
case "sensor": {
|
||||||
|
@@ -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";
|
import fuzzysort from "fuzzysort";
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||||
@@ -66,18 +18,48 @@ export interface ScorableTextItem {
|
|||||||
strings: string[];
|
strings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
export type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||||
filter: string,
|
filter: string,
|
||||||
items: T[]
|
items: T[]
|
||||||
) => T[];
|
) => T[];
|
||||||
|
|
||||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
|
export function fuzzyMatcher(search: string | null): (string) => boolean {
|
||||||
items
|
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) => {
|
.map((item) => {
|
||||||
item.score = fuzzySequentialMatch(filter, item);
|
item.score = scorer(item.strings);
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.filter((item) => item.score !== undefined)
|
.filter((item) => item.score !== undefined && item.score > -100000)
|
||||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 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) => {
|
const timeout = new Promise((_resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(`Timed out in ${ms} ms.`);
|
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);
|
||||||
|
};
|
@@ -347,8 +347,8 @@ class StatisticsChart extends LitElement {
|
|||||||
statTypes.forEach((type) => {
|
statTypes.forEach((type) => {
|
||||||
let val: number | null;
|
let val: number | null;
|
||||||
if (type === "sum") {
|
if (type === "sum") {
|
||||||
if (!initVal) {
|
if (initVal === null) {
|
||||||
initVal = val = stat.state;
|
initVal = val = stat.state || 0;
|
||||||
prevSum = stat.sum;
|
prevSum = stat.sum;
|
||||||
} else {
|
} else {
|
||||||
val = initVal + ((stat.sum || 0) - prevSum!);
|
val = initVal + ((stat.sum || 0) - prevSum!);
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
export const createCurrencyListEl = () => {
|
export const currencies = [
|
||||||
const list = document.createElement("datalist");
|
|
||||||
list.id = "currencies";
|
|
||||||
for (const currency of [
|
|
||||||
"AED",
|
"AED",
|
||||||
"AFN",
|
"AFN",
|
||||||
"ALL",
|
"ALL",
|
||||||
@@ -159,7 +156,12 @@ export const createCurrencyListEl = () => {
|
|||||||
"ZAR",
|
"ZAR",
|
||||||
"ZMK",
|
"ZMK",
|
||||||
"ZWL",
|
"ZWL",
|
||||||
]) {
|
];
|
||||||
|
|
||||||
|
export const createCurrencyListEl = () => {
|
||||||
|
const list = document.createElement("datalist");
|
||||||
|
list.id = "currencies";
|
||||||
|
for (const currency of currencies) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = currency;
|
option.value = currency;
|
||||||
option.innerHTML = currency;
|
option.innerHTML = currency;
|
||||||
|
@@ -7,25 +7,26 @@ import type {
|
|||||||
SortableColumnContainer,
|
SortableColumnContainer,
|
||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
|
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
|
||||||
|
|
||||||
const filterData = (
|
const filterData = (
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
columns: SortableColumnContainer,
|
columns: SortableColumnContainer,
|
||||||
filter: string
|
filter: string
|
||||||
) => {
|
) => {
|
||||||
filter = filter.toUpperCase();
|
const matcher = fuzzyMatcher(filter);
|
||||||
return data.filter((row) =>
|
return data.filter((row) =>
|
||||||
Object.entries(columns).some((columnEntry) => {
|
Object.entries(columns).some((columnEntry) => {
|
||||||
const [key, column] = columnEntry;
|
const [key, column] = columnEntry;
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
if (
|
if (
|
||||||
|
matcher(
|
||||||
String(
|
String(
|
||||||
column.filterKey
|
column.filterKey
|
||||||
? row[column.valueColumn || key][column.filterKey]
|
? row[column.valueColumn || key][column.filterKey]
|
||||||
: row[column.valueColumn || key]
|
: row[column.valueColumn || key]
|
||||||
)
|
)
|
||||||
.toUpperCase()
|
)
|
||||||
.includes(filter)
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -198,7 +198,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
this.hass,
|
this.hass,
|
||||||
deviceEntityLookup[device.id]
|
deviceEntityLookup[device.id]
|
||||||
),
|
),
|
||||||
area: device.area_id
|
area:
|
||||||
|
device.area_id && areaLookup[device.area_id]
|
||||||
? areaLookup[device.area_id].name
|
? areaLookup[device.area_id].name
|
||||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||||
}));
|
}));
|
||||||
|
@@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "./ha-device-picker";
|
import "./ha-device-picker";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
|
||||||
|
|
||||||
@customElement("ha-devices-picker")
|
@customElement("ha-devices-picker")
|
||||||
class HaDevicesPicker extends LitElement {
|
class HaDevicesPicker extends LitElement {
|
||||||
@@ -13,6 +14,8 @@ class HaDevicesPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required?: boolean;
|
@property({ type: Boolean }) public required?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +42,8 @@ class HaDevicesPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
|
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
|
||||||
|
|
||||||
|
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -53,11 +58,13 @@ class HaDevicesPicker extends LitElement {
|
|||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedDeviceLabel}
|
.label=${this.pickedDeviceLabel}
|
||||||
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._deviceChanged}
|
@value-changed=${this._deviceChanged}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,12 +72,15 @@ class HaDevicesPicker extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
|
allow-custom-entity
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.label=${this.pickDeviceLabel}
|
.label=${this.pickDeviceLabel}
|
||||||
|
.disabled=${this.disabled}
|
||||||
.required=${this.required && !currentDevices.length}
|
.required=${this.required && !currentDevices.length}
|
||||||
@value-changed=${this._addDevice}
|
@value-changed=${this._addDevice}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
|
@@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Array }) public value?: string[];
|
@property({ type: Array }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required?: boolean;
|
@property({ type: Boolean }) public required?: boolean;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
@@ -96,6 +98,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedEntityLabel}
|
.label=${this.pickedEntityLabel}
|
||||||
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._entityChanged}
|
@value-changed=${this._entityChanged}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,6 +106,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
|
allow-custom-entity
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
@@ -113,6 +117,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.entityFilter=${this._entityFilter}
|
.entityFilter=${this._entityFilter}
|
||||||
.label=${this.pickEntityLabel}
|
.label=${this.pickEntityLabel}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
.required=${this.required && !currentEntities.length}
|
.required=${this.required && !currentEntities.length}
|
||||||
@value-changed=${this._addEntity}
|
@value-changed=${this._addEntity}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@@ -15,6 +15,7 @@ import type { HaComboBox } from "../ha-combo-box";
|
|||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||||
|
|
||||||
interface HassEntityWithCachedName extends HassEntity {
|
interface HassEntityWithCachedName extends HassEntity {
|
||||||
friendly_name: string;
|
friendly_name: string;
|
||||||
@@ -336,11 +337,18 @@ export class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
const filterString = ev.detail.value.toLowerCase();
|
const filterString = ev.detail.value;
|
||||||
(this.comboBox as any).filteredItems = this._states.filter(
|
|
||||||
(entityState) =>
|
const sortableEntityStates = this._states.map((entityState) => ({
|
||||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
strings: [entityState.entity_id, computeStateName(entityState)],
|
||||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
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 { haStyle } from "../resources/styles";
|
||||||
import { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-checkbox";
|
|
||||||
import type { HaCheckbox } from "./ha-checkbox";
|
|
||||||
import "./ha-settings-row";
|
import "./ha-settings-row";
|
||||||
|
import "./ha-switch";
|
||||||
|
import type { HaSwitch } from "./ha-switch";
|
||||||
|
|
||||||
const ADDITIONAL_PREFERENCES = [
|
const ADDITIONAL_PREFERENCES = [
|
||||||
{
|
{
|
||||||
@@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
<span slot="prefix">
|
<span slot="heading" data-for="base"> Basic analytics </span>
|
||||||
<ha-checkbox
|
<span slot="description" data-for="base">
|
||||||
@change=${this._handleRowCheckboxClick}
|
This includes information about your system.
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._handleRowClick}
|
||||||
.checked=${baseEnabled}
|
.checked=${baseEnabled}
|
||||||
.preference=${"base"}
|
.preference=${"base"}
|
||||||
.disabled=${loading}
|
.disabled=${loading}
|
||||||
name="base"
|
name="base"
|
||||||
>
|
>
|
||||||
</ha-checkbox>
|
</ha-switch>
|
||||||
</span>
|
|
||||||
<span slot="heading" data-for="base"> Basic analytics </span>
|
|
||||||
<span slot="description" data-for="base">
|
|
||||||
This includes information about your system.
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
${ADDITIONAL_PREFERENCES.map(
|
${ADDITIONAL_PREFERENCES.map(
|
||||||
(preference) =>
|
(preference) =>
|
||||||
html`<ha-settings-row>
|
html`
|
||||||
<span slot="prefix">
|
<ha-settings-row>
|
||||||
<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}>
|
<span slot="heading" data-for=${preference.key}>
|
||||||
${preference.title}
|
${preference.title}
|
||||||
</span>
|
</span>
|
||||||
<span slot="description" data-for=${preference.key}>
|
<span slot="description" data-for=${preference.key}>
|
||||||
${preference.description}
|
${preference.description}
|
||||||
</span>
|
</span>
|
||||||
</ha-settings-row>`
|
<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>
|
<ha-settings-row>
|
||||||
<span slot="prefix">
|
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
|
||||||
<ha-checkbox
|
<span slot="description" data-for="diagnostics">
|
||||||
@change=${this._handleRowCheckboxClick}
|
Share crash reports when unexpected errors occur.
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._handleRowClick}
|
||||||
.checked=${this.analytics?.preferences.diagnostics}
|
.checked=${this.analytics?.preferences.diagnostics}
|
||||||
.preference=${"diagnostics"}
|
.preference=${"diagnostics"}
|
||||||
.disabled=${loading}
|
.disabled=${loading}
|
||||||
name="diagnostics"
|
name="diagnostics"
|
||||||
>
|
>
|
||||||
</ha-checkbox>
|
</ha-switch>
|
||||||
</span>
|
|
||||||
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
|
|
||||||
<span slot="description" data-for="diagnostics">
|
|
||||||
Share crash reports when unexpected errors occur.
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowCheckboxClick(ev: Event) {
|
private _handleRowClick(ev: Event) {
|
||||||
const checkbox = ev.currentTarget as HaCheckbox;
|
const target = ev.currentTarget as HaSwitch;
|
||||||
const preference = (checkbox as any).preference;
|
const preference = (target as any).preference;
|
||||||
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
|
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
|
||||||
|
|
||||||
if (preferences[preference] === checkbox.checked) {
|
if (preferences[preference] === target.checked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences[preference] = checkbox.checked;
|
preferences[preference] = target.checked;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
|
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
|
||||||
checkbox.checked
|
target.checked
|
||||||
) {
|
) {
|
||||||
preferences.base = true;
|
preferences.base = true;
|
||||||
} else if (preference === "base" && !checkbox.checked) {
|
} else if (preference === "base" && !target.checked) {
|
||||||
preferences.usage = false;
|
preferences.usage = false;
|
||||||
preferences.statistics = false;
|
preferences.statistics = false;
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,8 @@ interface State {
|
|||||||
backgroundColor: null | string;
|
backgroundColor: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hassio-ansi-to-html")
|
@customElement("ha-ansi-to-html")
|
||||||
class HassioAnsiToHtml extends LitElement {
|
class HaAnsiToHtml extends LitElement {
|
||||||
@property() public content!: string;
|
@property() public content!: string;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -241,6 +241,6 @@ class HassioAnsiToHtml extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hassio-ansi-to-html": HassioAnsiToHtml;
|
"ha-ansi-to-html": HaAnsiToHtml;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
this._areas = [...this._areas!, area];
|
this._areas = [...this._areas!, area];
|
||||||
(this.comboBox as any).items = this._getAreas(
|
(this.comboBox as any).filteredItems = this._getAreas(
|
||||||
this._areas!,
|
this._areas!,
|
||||||
this._devices!,
|
this._devices!,
|
||||||
this._entities!,
|
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;
|
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
@@ -117,6 +117,19 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
--mdc-shape-small: 4px;
|
--mdc-shape-small: 4px;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([dir="rtl"]) ha-icon-button:first-child,
|
||||||
|
:host([dir="rtl"]) mwc-button:first-child {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
border-right-width: 1px;
|
||||||
|
--mdc-shape-small: 0 4px 4px 0;
|
||||||
|
--mdc-button-outline-width: 1px;
|
||||||
|
}
|
||||||
|
:host([dir="rtl"]) ha-icon-button:last-child,
|
||||||
|
:host([dir="rtl"]) mwc-button:last-child {
|
||||||
|
--mdc-shape-small: 4px 0 0 4px;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
top: -7px;
|
||||||
right: 36px;
|
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-left: 40px;
|
||||||
margin-right: 0px;
|
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 { Fab } from "@material/mwc-fab";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
import { css } from "lit";
|
||||||
|
|
||||||
@customElement("ha-fab")
|
@customElement("ha-fab")
|
||||||
export class HaFab extends Fab {
|
export class HaFab extends Fab {
|
||||||
@@ -7,6 +8,17 @@ export class HaFab extends Fab {
|
|||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
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 {
|
declare global {
|
||||||
|
@@ -176,10 +176,24 @@ export class HaFileUpload extends LitElement {
|
|||||||
.mdc-text-field__icon--leading {
|
.mdc-text-field__icon--leading {
|
||||||
margin-bottom: 12px;
|
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 {
|
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||||
transform: scale(0.75);
|
transform: scale(0.75);
|
||||||
top: 8px;
|
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 {
|
.dragged:before {
|
||||||
position: var(--layout-fit_-_position);
|
position: var(--layout-fit_-_position);
|
||||||
top: var(--layout-fit_-_top);
|
top: var(--layout-fit_-_top);
|
||||||
|
@@ -53,7 +53,7 @@ class HaLabeledSlider extends PolymerElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getTitle() {
|
_getTitle() {
|
||||||
return `${this.caption}${this.required ? "*" : ""}`;
|
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
|
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 {
|
ha-settings-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
--paper-time-input-justify-content: flex-end;
|
||||||
|
--settings-row-content-display: contents;
|
||||||
|
--settings-row-prefix-display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
span[slot="heading"],
|
span[slot="heading"],
|
||||||
|
@@ -47,6 +47,10 @@ export class HaSelect extends SelectBase {
|
|||||||
.mdc-select__anchor {
|
.mdc-select__anchor {
|
||||||
width: var(--ha-select-min-width, 200px);
|
width: var(--ha-select-min-width, 200px);
|
||||||
}
|
}
|
||||||
|
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||||
|
right: 16px !important;
|
||||||
|
left: initial !important;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ export class HaColorTempSelector extends LitElement {
|
|||||||
<ha-labeled-slider
|
<ha-labeled-slider
|
||||||
pin
|
pin
|
||||||
icon="hass:thermometer"
|
icon="hass:thermometer"
|
||||||
.caption=${this.label}
|
.caption=${this.label || ""}
|
||||||
.min=${this.selector.color_temp.min_mireds ?? 153}
|
.min=${this.selector.color_temp.min_mireds ?? 153}
|
||||||
.max=${this.selector.color_temp.max_mireds ?? 500}
|
.max=${this.selector.color_temp.max_mireds ?? 500}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
|
@@ -66,12 +66,14 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.deviceFilter=${this._filterDevices}
|
||||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||||
? [this.selector.device.entity.device_class]
|
? [this.selector.device.entity.device_class]
|
||||||
: undefined}
|
: undefined}
|
||||||
.includeDomains=${this.selector.device.entity?.domain
|
.includeDomains=${this.selector.device.entity?.domain
|
||||||
? [this.selector.device.entity.domain]
|
? [this.selector.device.entity.domain]
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></ha-devices-picker>
|
></ha-devices-picker>
|
||||||
`;
|
`;
|
||||||
|
@@ -51,9 +51,10 @@ export class HaEntitySelector extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.entityFilter=${this._filterEntities}
|
|
||||||
.includeEntities=${this.selector.entity.include_entities}
|
.includeEntities=${this.selector.entity.include_entities}
|
||||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||||
|
.entityFilter=${this._filterEntities}
|
||||||
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></ha-entities-picker>
|
></ha-entities-picker>
|
||||||
`;
|
`;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -76,6 +76,13 @@ export class HaLocationSelector extends LitElement {
|
|||||||
const radius = ev.detail.radius;
|
const radius = ev.detail.radius;
|
||||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -30,7 +30,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
const isBox = this.selector.number.mode === "box";
|
const isBox = this.selector.number.mode === "box";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label}${this.required ? "*" : ""}
|
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
|
||||||
<div class="input">
|
<div class="input">
|
||||||
${!isBox
|
${!isBox
|
||||||
? html`<ha-slider
|
? html`<ha-slider
|
||||||
@@ -107,6 +107,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
ha-slider {
|
ha-slider {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-yaml-editor";
|
import "../ha-yaml-editor";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
|
|
||||||
@customElement("ha-selector-object")
|
@customElement("ha-selector-object")
|
||||||
export class HaObjectSelector extends LitElement {
|
export class HaObjectSelector extends LitElement {
|
||||||
@@ -12,6 +13,8 @@ export class HaObjectSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
@property() public placeholder?: string;
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
@@ -22,11 +25,15 @@ export class HaObjectSelector extends LitElement {
|
|||||||
return html`<ha-yaml-editor
|
return html`<ha-yaml-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.readonly=${this.disabled}
|
.readonly=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
.defaultValue=${this.value}
|
.defaultValue=${this.value}
|
||||||
@value-changed=${this._handleChange}
|
@value-changed=${this._handleChange}
|
||||||
></ha-yaml-editor>`;
|
></ha-yaml-editor>
|
||||||
|
${this.helper
|
||||||
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
|
: ""} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleChange(ev) {
|
private _handleChange(ev) {
|
||||||
|
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-object";
|
||||||
import "./ha-selector-select";
|
import "./ha-selector-select";
|
||||||
import "./ha-selector-target";
|
import "./ha-selector-target";
|
||||||
|
import "./ha-selector-template";
|
||||||
import "./ha-selector-text";
|
import "./ha-selector-text";
|
||||||
import "./ha-selector-time";
|
import "./ha-selector-time";
|
||||||
import "./ha-selector-icon";
|
import "./ha-selector-icon";
|
||||||
|
@@ -472,6 +472,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
ha-settings-row {
|
ha-settings-row {
|
||||||
--paper-time-input-justify-content: flex-end;
|
--paper-time-input-justify-content: flex-end;
|
||||||
--settings-row-content-width: 100%;
|
--settings-row-content-width: 100%;
|
||||||
|
--settings-row-prefix-display: contents;
|
||||||
border-top: var(
|
border-top: var(
|
||||||
--service-control-items-border-top,
|
--service-control-items-border-top,
|
||||||
1px solid var(--divider-color)
|
1px solid var(--divider-color)
|
||||||
|
@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
|
|||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
:host(:not([narrow])) .content {
|
:host(:not([narrow])) .content {
|
||||||
display: flex;
|
display: var(--settings-row-content-display, flex);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
@@ -68,7 +68,7 @@ export class HaSettingsRow extends LitElement {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.prefix-wrap {
|
.prefix-wrap {
|
||||||
display: contents;
|
display: var(--settings-row-prefix-display);
|
||||||
}
|
}
|
||||||
:host([narrow]) .prefix-wrap {
|
:host([narrow]) .prefix-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -1039,6 +1039,8 @@ class HaSidebar extends LitElement {
|
|||||||
|
|
||||||
.notification-badge,
|
.notification-badge,
|
||||||
.configuration-badge {
|
.configuration-badge {
|
||||||
|
left: calc(var(--app-drawer-width) - 42px);
|
||||||
|
position: absolute;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -1049,9 +1051,6 @@ class HaSidebar extends LitElement {
|
|||||||
padding: 0px 6px;
|
padding: 0px 6px;
|
||||||
color: var(--text-accent-color, var(--text-primary-color));
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
}
|
}
|
||||||
.configuration-badge {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-svg-icon + .notification-badge,
|
ha-svg-icon + .notification-badge,
|
||||||
ha-svg-icon + .configuration-badge {
|
ha-svg-icon + .configuration-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -616,6 +616,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
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 {
|
.mdc-text-field {
|
||||||
overflow: var(--text-field-overflow);
|
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;
|
||||||
|
}
|
||||||
|
}
|
@@ -61,7 +61,9 @@ export class HaYamlEditor extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
|
${this.label
|
||||||
|
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
|
||||||
|
: ""}
|
||||||
<ha-code-editor
|
<ha-code-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._yaml}
|
.value=${this._yaml}
|
||||||
|
@@ -19,9 +19,9 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
import "./ha-map";
|
import "./ha-map";
|
||||||
import type { HaMap } from "./ha-map";
|
import type { HaMap } from "./ha-map";
|
||||||
import "../ha-input-helper-text";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -297,7 +297,7 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
ha-map {
|
ha-map {
|
||||||
display: block;
|
display: block;
|
||||||
height: 300px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -151,6 +151,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
--media-browser-max-height: calc(100vh - 65px);
|
--media-browser-max-height: calc(100vh - 65px);
|
||||||
|
height: calc(100vh - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
@@ -163,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
position: initial;
|
position: initial;
|
||||||
--media-browser-max-height: 100vh - 137px;
|
--media-browser-max-height: 100vh - 137px;
|
||||||
|
height: 100vh - 137px;
|
||||||
width: 700px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,8 @@ import "@material/mwc-list/mwc-list";
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||||
|
import "@lit-labs/virtualizer";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -16,16 +18,13 @@ import {
|
|||||||
eventOptions,
|
eventOptions,
|
||||||
property,
|
property,
|
||||||
query,
|
query,
|
||||||
queryAll,
|
|
||||||
state,
|
state,
|
||||||
} from "lit/decorators";
|
} from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { until } from "lit/directives/until";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { getSignedPath } from "../../data/auth";
|
|
||||||
import type { MediaPlayerItem } from "../../data/media-player";
|
import type { MediaPlayerItem } from "../../data/media-player";
|
||||||
import {
|
import {
|
||||||
browseMediaPlayer,
|
browseMediaPlayer,
|
||||||
@@ -40,18 +39,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
|||||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import "../entity/ha-entity-picker";
|
import "../entity/ha-entity-picker";
|
||||||
import "../ha-button-menu";
|
import "../ha-button-menu";
|
||||||
import "../ha-card";
|
import "../ha-card";
|
||||||
import type { HaCard } from "../ha-card";
|
|
||||||
import "../ha-circular-progress";
|
import "../ha-circular-progress";
|
||||||
import "../ha-fab";
|
import "../ha-fab";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./ha-browse-media-tts";
|
import "./ha-browse-media-tts";
|
||||||
import type { TtsMediaPickedEvent } from "./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 {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@@ -101,8 +100,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
@query(".content") private _content?: HTMLDivElement;
|
@query(".content") private _content?: HTMLDivElement;
|
||||||
|
|
||||||
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
|
|
||||||
|
|
||||||
private _headerOffsetHeight = 0;
|
private _headerOffsetHeight = 0;
|
||||||
|
|
||||||
private _resizeObserver?: ResizeObserver;
|
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 {
|
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||||
super.willUpdate(changedProps);
|
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 {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
@@ -590,16 +280,368 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
this._animateHeaderHeight();
|
this._animateHeaderHeight();
|
||||||
} else if (changedProps.has("_currentItem")) {
|
} else if (changedProps.has("_currentItem")) {
|
||||||
this._setHeaderHeight();
|
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();
|
ev.stopPropagation();
|
||||||
const item = (ev.currentTarget as any).item;
|
const item = (ev.currentTarget as any).item;
|
||||||
|
|
||||||
this._runAction(item);
|
this._runAction(item);
|
||||||
}
|
};
|
||||||
|
|
||||||
private _runAction(item: MediaPlayerItem): void {
|
private _runAction(item: MediaPlayerItem): void {
|
||||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
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 target = ev.currentTarget as any;
|
||||||
const item: MediaPlayerItem = target.item;
|
const item: MediaPlayerItem = target.item;
|
||||||
|
|
||||||
@@ -631,7 +673,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
fireEvent(this, "media-browsed", {
|
fireEvent(this, "media-browsed", {
|
||||||
ids: [...this.navigateIds, item],
|
ids: [...this.navigateIds, item],
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private async _fetchData(
|
private async _fetchData(
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@@ -658,55 +700,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
this._resizeObserver.observe(this);
|
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 {
|
private _closeDialogAction(): void {
|
||||||
fireEvent(this, "close-dialog");
|
fireEvent(this, "close-dialog");
|
||||||
}
|
}
|
||||||
@@ -841,6 +834,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
.content {
|
.content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HEADER */
|
/* HEADER */
|
||||||
@@ -926,6 +920,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
.not-shown {
|
.not-shown {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
padding: 8px 16px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid.not-shown {
|
.grid.not-shown {
|
||||||
@@ -951,7 +946,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
border-bottom-color: var(--divider-color);
|
border-bottom-color: var(--divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.children {
|
mwc-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.children {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
auto-fit,
|
auto-fit,
|
||||||
@@ -988,7 +987,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
padding-bottom: 100%;
|
padding-bottom: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portrait.children ha-card .thumbnail {
|
.portrait ha-card .thumbnail {
|
||||||
padding-bottom: 150%;
|
padding-bottom: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,10 +1061,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-card:hover .lazythumbnail {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.child .title {
|
.child .title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@@ -1127,7 +1122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([narrow]) .children {
|
:host([narrow]) div.children {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
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-fab-box-shadow: none;
|
||||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
--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);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,8 @@ import { BlueprintInput } from "./blueprint";
|
|||||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import { Action, MODES } from "./script";
|
import { Action, MODES } from "./script";
|
||||||
|
|
||||||
|
export const AUTOMATION_DEFAULT_MODE: ManualAutomationConfig["mode"] = "single";
|
||||||
|
|
||||||
export interface AutomationEntity extends HassEntityBase {
|
export interface AutomationEntity extends HassEntityBase {
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -67,7 +69,7 @@ export interface BaseTrigger {
|
|||||||
|
|
||||||
export interface StateTrigger extends BaseTrigger {
|
export interface StateTrigger extends BaseTrigger {
|
||||||
platform: "state";
|
platform: "state";
|
||||||
entity_id: string;
|
entity_id: string | string[];
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
from?: string | number;
|
from?: string | number;
|
||||||
to?: string | string[] | number;
|
to?: string | string[] | number;
|
||||||
@@ -150,6 +152,12 @@ export interface EventTrigger extends BaseTrigger {
|
|||||||
context?: ContextConstraint;
|
context?: ContextConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CalendarTrigger extends BaseTrigger {
|
||||||
|
platform: "calendar";
|
||||||
|
event: "start" | "end";
|
||||||
|
entity_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Trigger =
|
export type Trigger =
|
||||||
| StateTrigger
|
| StateTrigger
|
||||||
| MqttTrigger
|
| MqttTrigger
|
||||||
@@ -164,7 +172,8 @@ export type Trigger =
|
|||||||
| TimeTrigger
|
| TimeTrigger
|
||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| EventTrigger
|
||||||
| DeviceTrigger;
|
| DeviceTrigger
|
||||||
|
| CalendarTrigger;
|
||||||
|
|
||||||
interface BaseCondition {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
@@ -224,6 +233,20 @@ export interface TriggerCondition extends BaseCondition {
|
|||||||
id: string;
|
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 =
|
export type Condition =
|
||||||
| StateCondition
|
| StateCondition
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
@@ -235,6 +258,12 @@ export type Condition =
|
|||||||
| LogicalCondition
|
| LogicalCondition
|
||||||
| TriggerCondition;
|
| TriggerCondition;
|
||||||
|
|
||||||
|
export type ConditionWithShorthand =
|
||||||
|
| Condition
|
||||||
|
| ShorthandAndCondition
|
||||||
|
| ShorthandOrCondition
|
||||||
|
| ShorthandNotCondition;
|
||||||
|
|
||||||
export const triggerAutomationActions = (
|
export const triggerAutomationActions = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string
|
entityId: string
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface LogProvider {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchErrorLog = (hass: HomeAssistant) =>
|
export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||||
hass.callApi<string>("GET", "error_log");
|
hass.callApi<string>("GET", "error_log");
|
||||||
|
@@ -21,7 +21,8 @@ export type AddonState = "started" | "stopped" | null;
|
|||||||
export type AddonRepository = "core" | "local" | string;
|
export type AddonRepository = "core" | "local" | string;
|
||||||
|
|
||||||
interface AddonTranslations {
|
interface AddonTranslations {
|
||||||
[key: string]: Record<string, Record<string, Record<string, string>>>;
|
network?: Record<string, string>;
|
||||||
|
configuration?: Record<string, { name?: string; description?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HassioAddonInfo {
|
export interface HassioAddonInfo {
|
||||||
@@ -91,7 +92,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
|||||||
slug: string;
|
slug: string;
|
||||||
startup: AddonStartup;
|
startup: AddonStartup;
|
||||||
stdin: boolean;
|
stdin: boolean;
|
||||||
translations: AddonTranslations;
|
translations: Record<string, AddonTranslations>;
|
||||||
watchdog: null | boolean;
|
watchdog: null | boolean;
|
||||||
webui: null | string;
|
webui: null | string;
|
||||||
}
|
}
|
||||||
|
@@ -194,11 +194,24 @@ export interface ChooseAction {
|
|||||||
default?: Action | Action[];
|
default?: Action | Action[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IfAction {
|
||||||
|
alias?: string;
|
||||||
|
if: string | Condition[];
|
||||||
|
then: Action | Action[];
|
||||||
|
else?: Action | Action[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface VariablesAction {
|
export interface VariablesAction {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
variables: Record<string, unknown>;
|
variables: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StopAction {
|
||||||
|
alias?: string;
|
||||||
|
stop: string;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface UnknownAction {
|
interface UnknownAction {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
@@ -215,8 +228,10 @@ export type Action =
|
|||||||
| WaitForTriggerAction
|
| WaitForTriggerAction
|
||||||
| RepeatAction
|
| RepeatAction
|
||||||
| ChooseAction
|
| ChooseAction
|
||||||
|
| IfAction
|
||||||
| VariablesAction
|
| VariablesAction
|
||||||
| PlayMediaAction
|
| PlayMediaAction
|
||||||
|
| StopAction
|
||||||
| UnknownAction;
|
| UnknownAction;
|
||||||
|
|
||||||
export interface ActionTypes {
|
export interface ActionTypes {
|
||||||
@@ -228,10 +243,12 @@ export interface ActionTypes {
|
|||||||
activate_scene: SceneAction;
|
activate_scene: SceneAction;
|
||||||
repeat: RepeatAction;
|
repeat: RepeatAction;
|
||||||
choose: ChooseAction;
|
choose: ChooseAction;
|
||||||
|
if: IfAction;
|
||||||
wait_for_trigger: WaitForTriggerAction;
|
wait_for_trigger: WaitForTriggerAction;
|
||||||
variables: VariablesAction;
|
variables: VariablesAction;
|
||||||
service: ServiceAction;
|
service: ServiceAction;
|
||||||
play_media: PlayMediaAction;
|
play_media: PlayMediaAction;
|
||||||
|
stop: StopAction;
|
||||||
unknown: UnknownAction;
|
unknown: UnknownAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,12 +316,18 @@ export const getActionType = (action: Action): ActionType => {
|
|||||||
if ("choose" in action) {
|
if ("choose" in action) {
|
||||||
return "choose";
|
return "choose";
|
||||||
}
|
}
|
||||||
|
if ("if" in action) {
|
||||||
|
return "if";
|
||||||
|
}
|
||||||
if ("wait_for_trigger" in action) {
|
if ("wait_for_trigger" in action) {
|
||||||
return "wait_for_trigger";
|
return "wait_for_trigger";
|
||||||
}
|
}
|
||||||
if ("variables" in action) {
|
if ("variables" in action) {
|
||||||
return "variables";
|
return "variables";
|
||||||
}
|
}
|
||||||
|
if ("stop" in action) {
|
||||||
|
return "stop";
|
||||||
|
}
|
||||||
if ("service" in action) {
|
if ("service" in action) {
|
||||||
if ("metadata" in action) {
|
if ("metadata" in action) {
|
||||||
if (is(action, activateSceneActionStruct)) {
|
if (is(action, activateSceneActionStruct)) {
|
||||||
|
@@ -19,6 +19,7 @@ export type Selector =
|
|||||||
| SelectSelector
|
| SelectSelector
|
||||||
| StringSelector
|
| StringSelector
|
||||||
| TargetSelector
|
| TargetSelector
|
||||||
|
| TemplateSelector
|
||||||
| ThemeSelector
|
| ThemeSelector
|
||||||
| TimeSelector;
|
| TimeSelector;
|
||||||
|
|
||||||
@@ -213,6 +214,11 @@ export interface TargetSelector {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TemplateSelector {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
template: {};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ThemeSelector {
|
export interface ThemeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
theme: {};
|
theme: {};
|
||||||
|
@@ -1,10 +1,15 @@
|
|||||||
import type {
|
import type {
|
||||||
|
HassEntities,
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { BINARY_STATE_ON } from "../common/const";
|
import { BINARY_STATE_ON } from "../common/const";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
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 { HomeAssistant } from "../types";
|
||||||
|
import { showToast } from "../util/toast";
|
||||||
|
|
||||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||||
@@ -31,8 +36,12 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
|||||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||||
typeof entity.attributes.in_progress === "number";
|
typeof entity.attributes.in_progress === "number";
|
||||||
|
|
||||||
export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
export const updateCanInstall = (
|
||||||
entity.state === BINARY_STATE_ON &&
|
entity: UpdateEntity,
|
||||||
|
showSkipped = false
|
||||||
|
): boolean =>
|
||||||
|
(entity.state === BINARY_STATE_ON ||
|
||||||
|
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
|
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
|
||||||
|
|
||||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||||
@@ -43,3 +52,75 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
|||||||
type: "update/release_notes",
|
type: "update/release_notes",
|
||||||
entity_id: entityId,
|
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[];
|
entity_ids?: string | string[];
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
strict?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
|
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
|
||||||
|
@@ -127,7 +127,7 @@ export interface ZWaveJSClient {
|
|||||||
|
|
||||||
export interface ZWaveJSController {
|
export interface ZWaveJSController {
|
||||||
home_id: number;
|
home_id: number;
|
||||||
library_version: string;
|
sdk_version: string;
|
||||||
type: number;
|
type: number;
|
||||||
own_node_id: number;
|
own_node_id: number;
|
||||||
is_secondary: boolean;
|
is_secondary: boolean;
|
||||||
@@ -136,7 +136,7 @@ export interface ZWaveJSController {
|
|||||||
was_real_primary: boolean;
|
was_real_primary: boolean;
|
||||||
is_static_update_controller: boolean;
|
is_static_update_controller: boolean;
|
||||||
is_slave: boolean;
|
is_slave: boolean;
|
||||||
serial_api_version: string;
|
firmware_version: string;
|
||||||
manufacturer_id: number;
|
manufacturer_id: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
product_type: number;
|
product_type: number;
|
||||||
|
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
@@ -261,7 +260,6 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass.localize("ui.common.help")}
|
.label=${this.hass.localize("ui.common.help")}
|
||||||
.path=${mdiHelpCircle}
|
.path=${mdiHelpCircle}
|
||||||
?rtl=${computeRTL(this.hass)}
|
|
||||||
>
|
>
|
||||||
</ha-icon-button
|
</ha-icon-button
|
||||||
></a>
|
></a>
|
||||||
@@ -273,7 +271,6 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
)}
|
)}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
?rtl=${computeRTL(this.hass)}
|
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
${this._step === null
|
${this._step === null
|
||||||
@@ -521,7 +518,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
.dialog-actions[rtl] {
|
:host-context([style*="direction: rtl;"]) .dialog-actions {
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
@@ -146,14 +146,14 @@ export const showOptionsFlowDialog = (
|
|||||||
renderMenuHeader(hass, step) {
|
renderMenuHeader(hass, step) {
|
||||||
return (
|
return (
|
||||||
hass.localize(
|
hass.localize(
|
||||||
`component.${step.handler}.option.step.${step.step_id}.title`
|
`component.${configEntry.domain}.options.step.${step.step_id}.title`
|
||||||
) || hass.localize(`component.${step.handler}.title`)
|
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderMenuDescription(hass, step) {
|
renderMenuDescription(hass, step) {
|
||||||
const description = hass.localize(
|
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
|
step.description_placeholders
|
||||||
);
|
);
|
||||||
return description
|
return description
|
||||||
@@ -169,7 +169,7 @@ export const showOptionsFlowDialog = (
|
|||||||
|
|
||||||
renderMenuOption(hass, step, option) {
|
renderMenuOption(hass, step, option) {
|
||||||
return hass.localize(
|
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
|
step.description_placeholders
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -28,7 +28,7 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
const localize = this.hass.localize;
|
const localize = this.hass.localize;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<h2>Success!</h2>
|
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
|
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
|
||||||
${this.step.result?.state === "not_loaded"
|
${this.step.result?.state === "not_loaded"
|
||||||
@@ -41,7 +41,11 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
${this.devices.length === 0
|
${this.devices.length === 0
|
||||||
? ""
|
? ""
|
||||||
: html`
|
: html`
|
||||||
<p>We found the following devices:</p>
|
<p>
|
||||||
|
${localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.found_following_devices"
|
||||||
|
)}:
|
||||||
|
</p>
|
||||||
<div class="devices">
|
<div class="devices">
|
||||||
${this.devices.map(
|
${this.devices.map(
|
||||||
(device) =>
|
(device) =>
|
||||||
@@ -49,7 +53,12 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
<div class="device">
|
<div class="device">
|
||||||
<div>
|
<div>
|
||||||
<b>${computeDeviceName(device, this.hass)}</b><br />
|
<b>${computeDeviceName(device, this.hass)}</b><br />
|
||||||
${device.model} (${device.manufacturer})
|
${!device.model && !device.manufacturer
|
||||||
|
? html` `
|
||||||
|
: html`${device.model}
|
||||||
|
${device.manufacturer
|
||||||
|
? html`(${device.manufacturer})`
|
||||||
|
: ""}`}
|
||||||
</div>
|
</div>
|
||||||
<ha-area-picker
|
<ha-area-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
@@ -194,6 +194,10 @@ class StepFlowForm extends LitElement {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding-right: 72px;
|
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 {
|
h2 {
|
||||||
padding-right: 66px;
|
padding-right: 66px;
|
||||||
}
|
}
|
||||||
|
:host-context([style*="direction: rtl;"]) h2 {
|
||||||
|
padding-right: auto !important;
|
||||||
|
padding-left: 66px !important;
|
||||||
|
}
|
||||||
@media all and (max-height: 900px) {
|
@media all and (max-height: 900px) {
|
||||||
div {
|
div {
|
||||||
max-height: calc(100vh - 134px);
|
max-height: calc(100vh - 134px);
|
||||||
|
@@ -313,6 +313,10 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
h2 {
|
h2 {
|
||||||
padding-right: 66px;
|
padding-right: 66px;
|
||||||
}
|
}
|
||||||
|
:host-context([style*="direction: rtl;"]) h2 {
|
||||||
|
padding-right: auto !important;
|
||||||
|
padding-left: 66px !important;
|
||||||
|
}
|
||||||
@media all and (max-height: 900px) {
|
@media all and (max-height: 900px) {
|
||||||
mwc-list {
|
mwc-list {
|
||||||
max-height: calc(100vh - 134px);
|
max-height: calc(100vh - 134px);
|
||||||
|
@@ -77,7 +77,7 @@ class DialogBox extends LitElement {
|
|||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
.value=${this._value || ""}
|
.value=${this._value || ""}
|
||||||
@keyup=${this._handleKeyUp}
|
@keyup=${this._handleKeyUp}
|
||||||
@change=${this._valueChanged}
|
@input=${this._valueChanged}
|
||||||
.label=${this._params.inputLabel
|
.label=${this._params.inputLabel
|
||||||
? this._params.inputLabel
|
? this._params.inputLabel
|
||||||
: ""}
|
: ""}
|
||||||
|
@@ -21,6 +21,7 @@ import {
|
|||||||
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
||||||
} from "../../../data/update";
|
} from "../../../data/update";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||||
|
|
||||||
@customElement("more-info-update")
|
@customElement("more-info-update")
|
||||||
class MoreInfoUpdate extends LitElement {
|
class MoreInfoUpdate extends LitElement {
|
||||||
@@ -56,13 +57,10 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
></mwc-linear-progress>`
|
></mwc-linear-progress>`
|
||||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.stateObj.attributes.title
|
<h3>${this.stateObj.attributes.title}</h3>
|
||||||
? html`<h3>${this.stateObj.attributes.title}</h3>`
|
|
||||||
: ""}
|
|
||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="key">
|
<div class="key">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@@ -132,11 +130,20 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
${this.stateObj.attributes.auto_update
|
${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`
|
: html`
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@click=${this._handleSkip}
|
@click=${this._handleSkip}
|
||||||
.disabled=${skippedVersion ||
|
.disabled=${skippedVersion ||
|
||||||
this.stateObj.state === "off" ||
|
this.stateObj.state === BINARY_STATE_OFF ||
|
||||||
updateIsInstalling(this.stateObj)}
|
updateIsInstalling(this.stateObj)}
|
||||||
>
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@@ -148,7 +155,7 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@click=${this._handleInstall}
|
@click=${this._handleInstall}
|
||||||
.disabled=${(this.stateObj.state === "off" &&
|
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||||
!skippedVersion) ||
|
!skippedVersion) ||
|
||||||
updateIsInstalling(this.stateObj)}
|
updateIsInstalling(this.stateObj)}
|
||||||
>
|
>
|
||||||
@@ -210,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 {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
hr {
|
hr {
|
||||||
|
@@ -119,7 +119,15 @@ class MoreInfoVacuum extends LitElement {
|
|||||||
"ui.dialogs.more_info_control.vacuum.status"
|
"ui.dialogs.more_info_control.vacuum.status"
|
||||||
)}:
|
)}:
|
||||||
</span>
|
</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>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
@@ -24,7 +24,7 @@ import { domainIcon } from "../../common/entity/domain_icon";
|
|||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
import {
|
import {
|
||||||
fuzzyFilterSort,
|
defaultFuzzyFilterSort,
|
||||||
ScorableTextItem,
|
ScorableTextItem,
|
||||||
} from "../../common/string/filter/sequence-matching";
|
} from "../../common/string/filter/sequence-matching";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
@@ -240,7 +240,7 @@ export class QuickBar extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`}
|
`}
|
||||||
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
|
${this._hint ? html`<ha-tip>${this._hint}</ha-tip>` : ""}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -694,7 +694,7 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
private _filterItems = memoizeOne(
|
private _filterItems = memoizeOne(
|
||||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
@@ -782,10 +782,8 @@ export class QuickBar extends LitElement {
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
ha-tip {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nothing-found {
|
.nothing-found {
|
||||||
|
@@ -5,15 +5,22 @@ import { property, state } from "lit/decorators";
|
|||||||
class HaInitPage extends LitElement {
|
class HaInitPage extends LitElement {
|
||||||
@property({ type: Boolean }) public error = false;
|
@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() {
|
protected render() {
|
||||||
return this.error
|
return this.error
|
||||||
? html`
|
? html`
|
||||||
<p>Unable to connect to Home Assistant.</p>
|
<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")
|
${location.host.includes("ui.nabu.casa")
|
||||||
? html`
|
? html`
|
||||||
<p>
|
<p>
|
||||||
@@ -29,7 +36,7 @@ class HaInitPage extends LitElement {
|
|||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div id="progress-indicator-wrapper">
|
<div id="progress-indicator-wrapper">
|
||||||
${this.showProgressIndicator
|
${this._showProgressIndicator
|
||||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -39,14 +46,26 @@ class HaInitPage extends LitElement {
|
|||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
if (this._showProgressIndicatorTimeout) {
|
||||||
clearTimeout(this._showProgressIndicatorTimeout);
|
clearTimeout(this._showProgressIndicatorTimeout);
|
||||||
}
|
}
|
||||||
|
if (this._retryInterval) {
|
||||||
|
clearInterval(this._retryInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._showProgressIndicatorTimeout = setTimeout(async () => {
|
this._showProgressIndicatorTimeout = setTimeout(async () => {
|
||||||
await import("../components/ha-circular-progress");
|
await import("../components/ha-circular-progress");
|
||||||
this.showProgressIndicator = true;
|
this._showProgressIndicator = true;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
this._retryInterval = setInterval(() => {
|
||||||
|
const remainingSeconds = this._retryInSeconds--;
|
||||||
|
if (remainingSeconds <= 0) {
|
||||||
|
this._retry();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _retry() {
|
private _retry() {
|
||||||
@@ -70,6 +89,9 @@ class HaInitPage extends LitElement {
|
|||||||
a {
|
a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
.retry-text {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
p,
|
p,
|
||||||
#loading-text {
|
#loading-text {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
|
@@ -130,7 +130,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
* Array of tabs to show on the page.
|
* Array of tabs to show on the page.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
*/
|
*/
|
||||||
@property() public tabs!: PageNavigation[];
|
@property() public tabs: PageNavigation[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force hides the filter menu.
|
* Force hides the filter menu.
|
||||||
@@ -283,6 +283,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
height: calc(100vh - 1px - var(--header-height));
|
height: calc(100vh - 1px - var(--header-height));
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
:host([narrow]) hass-tabs-subpage {
|
||||||
|
--main-title-margin: 0;
|
||||||
|
}
|
||||||
.table-header {
|
.table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -82,6 +82,16 @@ class HassTabsSubpage extends LitElement {
|
|||||||
(!page.advancedOnly || showAdvanced)
|
(!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(
|
return shownTabs.map(
|
||||||
(page) =>
|
(page) =>
|
||||||
html`
|
html`
|
||||||
@@ -134,7 +144,7 @@ class HassTabsSubpage extends LitElement {
|
|||||||
this.narrow,
|
this.narrow,
|
||||||
this.localizeFunc || this.hass.localize
|
this.localizeFunc || this.hass.localize
|
||||||
);
|
);
|
||||||
const showTabs = tabs.length > 1 || !this.narrow;
|
const showTabs = tabs.length > 1;
|
||||||
return html`
|
return html`
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
${this.mainPage || (!this.backPath && history.state?.root)
|
${this.mainPage || (!this.backPath && history.state?.root)
|
||||||
@@ -159,8 +169,10 @@ class HassTabsSubpage extends LitElement {
|
|||||||
@click=${this._backTapped}
|
@click=${this._backTapped}
|
||||||
></ha-icon-button-arrow-prev>
|
></ha-icon-button-arrow-prev>
|
||||||
`}
|
`}
|
||||||
${this.narrow
|
${this.narrow || !showTabs
|
||||||
? html`<div class="main-title"><slot name="header"></slot></div>`
|
? html`<div class="main-title">
|
||||||
|
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||||
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
${showTabs
|
${showTabs
|
||||||
? html`
|
? html`
|
||||||
@@ -283,6 +295,7 @@ class HassTabsSubpage extends LitElement {
|
|||||||
max-height: var(--header-height);
|
max-height: var(--header-height);
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: var(--sidebar-text-color);
|
color: var(--sidebar-text-color);
|
||||||
|
margin: var(--main-title-margin, 0 0 0 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
|
||||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-card";
|
import "../components/ha-card";
|
||||||
|
import "../components/ha-ansi-to-html";
|
||||||
import { fetchInstallationType } from "../data/onboarding";
|
import { fetchInstallationType } from "../data/onboarding";
|
||||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||||
@@ -86,7 +86,7 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
hassio-ansi-to-html {
|
ha-ansi-to-html {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
@@ -34,7 +34,10 @@ import { useAmPm } from "../../common/datetime/use_am_pm";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-button-toggle-group";
|
import "../../components/ha-button-toggle-group";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
|
import "../../components/ha-icon-button-prev";
|
||||||
|
import "../../components/ha-icon-button-next";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import type {
|
import type {
|
||||||
CalendarEvent,
|
CalendarEvent,
|
||||||
CalendarViewChanged,
|
CalendarViewChanged,
|
||||||
@@ -124,26 +127,25 @@ export class HAFullCalendar extends LitElement {
|
|||||||
"ui.components.calendar.today"
|
"ui.components.calendar.today"
|
||||||
)}</mwc-button
|
)}</mwc-button
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button-prev
|
||||||
.label=${this.hass.localize("ui.common.previous")}
|
.label=${this.hass.localize("ui.common.previous")}
|
||||||
.path=${mdiChevronLeft}
|
|
||||||
class="prev"
|
class="prev"
|
||||||
@click=${this._handlePrev}
|
@click=${this._handlePrev}
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button-prev>
|
||||||
<ha-icon-button
|
<ha-icon-button-next
|
||||||
.label=${this.hass.localize("ui.common.next")}
|
.label=${this.hass.localize("ui.common.next")}
|
||||||
.path=${mdiChevronRight}
|
|
||||||
class="next"
|
class="next"
|
||||||
@click=${this._handleNext}
|
@click=${this._handleNext}
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button-next>
|
||||||
</div>
|
</div>
|
||||||
<h1>${this.calendar.view.title}</h1>
|
<h1>${this.calendar.view.title}</h1>
|
||||||
<ha-button-toggle-group
|
<ha-button-toggle-group
|
||||||
.buttons=${viewToggleButtons}
|
.buttons=${viewToggleButtons}
|
||||||
.active=${this._activeView}
|
.active=${this._activeView}
|
||||||
@value-changed=${this._handleView}
|
@value-changed=${this._handleView}
|
||||||
|
.dir=${computeRTLDirection(this.hass)}
|
||||||
></ha-button-toggle-group>
|
></ha-button-toggle-group>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -179,6 +181,7 @@ export class HAFullCalendar extends LitElement {
|
|||||||
.buttons=${viewToggleButtons}
|
.buttons=${viewToggleButtons}
|
||||||
.active=${this._activeView}
|
.active=${this._activeView}
|
||||||
@value-changed=${this._handleView}
|
@value-changed=${this._handleView}
|
||||||
|
.dir=${computeRTLDirection(this.hass)}
|
||||||
></ha-button-toggle-group>
|
></ha-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
@@ -202,7 +202,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.tabs=${configSections.devices}
|
.tabs=${configSections.areas}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
>
|
>
|
||||||
${this.narrow
|
${this.narrow
|
||||||
|
@@ -82,7 +82,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.isWide=${this.isWide}
|
.isWide=${this.isWide}
|
||||||
back-path="/config"
|
back-path="/config"
|
||||||
.tabs=${configSections.devices}
|
.tabs=${configSections.areas}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
@@ -32,9 +32,11 @@ import "./types/ha-automation-action-condition";
|
|||||||
import "./types/ha-automation-action-delay";
|
import "./types/ha-automation-action-delay";
|
||||||
import "./types/ha-automation-action-device_id";
|
import "./types/ha-automation-action-device_id";
|
||||||
import "./types/ha-automation-action-event";
|
import "./types/ha-automation-action-event";
|
||||||
|
import "./types/ha-automation-action-if";
|
||||||
import "./types/ha-automation-action-play_media";
|
import "./types/ha-automation-action-play_media";
|
||||||
import "./types/ha-automation-action-repeat";
|
import "./types/ha-automation-action-repeat";
|
||||||
import "./types/ha-automation-action-service";
|
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_for_trigger";
|
||||||
import "./types/ha-automation-action-wait_template";
|
import "./types/ha-automation-action-wait_template";
|
||||||
|
|
||||||
@@ -49,7 +51,9 @@ const OPTIONS = [
|
|||||||
"wait_for_trigger",
|
"wait_for_trigger",
|
||||||
"repeat",
|
"repeat",
|
||||||
"choose",
|
"choose",
|
||||||
|
"if",
|
||||||
"device_id",
|
"device_id",
|
||||||
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const getType = (action: Action | undefined) => {
|
const getType = (action: Action | undefined) => {
|
||||||
@@ -410,7 +414,7 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
.rtl .card-menu {
|
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||||
right: initial;
|
right: initial;
|
||||||
left: 16px;
|
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",
|
name: "wait_template",
|
||||||
selector: {
|
selector: {
|
||||||
text: {
|
template: {},
|
||||||
multiline: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -188,7 +188,12 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||||||
([key, value]) =>
|
([key, value]) =>
|
||||||
html`<ha-settings-row .narrow=${this.narrow}>
|
html`<ha-settings-row .narrow=${this.narrow}>
|
||||||
<span slot="heading">${value?.name || key}</span>
|
<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
|
${value?.selector
|
||||||
? html`<ha-selector
|
? html`<ha-selector
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -327,6 +332,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||||||
ha-settings-row {
|
ha-settings-row {
|
||||||
--paper-time-input-justify-content: flex-end;
|
--paper-time-input-justify-content: flex-end;
|
||||||
--settings-row-content-width: 100%;
|
--settings-row-content-width: 100%;
|
||||||
|
--settings-row-prefix-display: contents;
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user