mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-18 17:39:48 +00:00
Compare commits
39 Commits
onboarding
...
20230831.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d187aa0ac6 | ||
![]() |
9c60a047c1 | ||
![]() |
1825749036 | ||
![]() |
93846a2867 | ||
![]() |
f3ed0160af | ||
![]() |
38b275f7f9 | ||
![]() |
c3a36efaa4 | ||
![]() |
68fa67e77a | ||
![]() |
806cebb024 | ||
![]() |
fa788a8223 | ||
![]() |
dfbaee1649 | ||
![]() |
cfb698d0a6 | ||
![]() |
63c3d6406d | ||
![]() |
d817e92a57 | ||
![]() |
96597b3963 | ||
![]() |
40c7bc08d9 | ||
![]() |
b8cd1760f7 | ||
![]() |
24dd45c8cd | ||
![]() |
e06bd41b5e | ||
![]() |
c0793fad83 | ||
![]() |
e002c5d96c | ||
![]() |
099e317d17 | ||
![]() |
ca1a183512 | ||
![]() |
c1cacf735e | ||
![]() |
515cfdb6d1 | ||
![]() |
3a6cffd6c1 | ||
![]() |
c84a826937 | ||
![]() |
c485e8d03e | ||
![]() |
2ab67328d4 | ||
![]() |
d350c35c4e | ||
![]() |
593b176ab8 | ||
![]() |
1a15c8da8c | ||
![]() |
060e67397a | ||
![]() |
d6de29ca8a | ||
![]() |
220767b347 | ||
![]() |
79e1fbe076 | ||
![]() |
7d80eb06b0 | ||
![]() |
a181189a49 | ||
![]() |
626b51112f |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@@ -31,6 +31,7 @@ export class HassioUploadBackup extends LitElement {
|
||||
.icon=${mdiFolderUpload}
|
||||
accept="application/x-tar"
|
||||
label="Upload backup"
|
||||
supports="Supports .TAR files"
|
||||
@file-picked=${this._uploadFile}
|
||||
auto-open-file-dialog
|
||||
></ha-file-upload>
|
||||
|
@@ -173,6 +173,7 @@ class HassioBackupDialog
|
||||
private async _restoreClicked() {
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._restoringBackup = true;
|
||||
this._dialogParams?.onRestoring?.();
|
||||
if (this._backupContent.backupType === "full") {
|
||||
await this._fullRestoreClicked(backupDetails);
|
||||
} else {
|
||||
@@ -219,7 +220,7 @@ class HassioBackupDialog
|
||||
this._error = error.body.message;
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
this._dialogParams?.onRestoring?.();
|
||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
@@ -268,7 +269,7 @@ class HassioBackupDialog
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
this._dialogParams?.onRestoring?.();
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
|
@@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
export interface HassioBackupDialogParams {
|
||||
slug: string;
|
||||
onDelete?: () => void;
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
|
13
package.json
13
package.json
@@ -28,10 +28,10 @@
|
||||
"@babel/runtime": "7.22.11",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@codemirror/autocomplete": "6.9.0",
|
||||
"@codemirror/commands": "6.2.4",
|
||||
"@codemirror/commands": "6.2.5",
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.1",
|
||||
"@codemirror/search": "6.5.2",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.16.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
@@ -82,7 +82,6 @@
|
||||
"@material/web": "=1.0.0-pre.16",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@polymer/app-layout": "3.1.0",
|
||||
"@polymer/iron-flex-layout": "3.0.1",
|
||||
"@polymer/iron-input": "3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "3.0.1",
|
||||
@@ -103,10 +102,10 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "3.3.2",
|
||||
"chart.js": "4.3.3",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.32.1",
|
||||
"cropperjs": "1.5.13",
|
||||
"cropperjs": "1.6.0",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
@@ -122,7 +121,7 @@
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.2",
|
||||
"marked": "7.0.4",
|
||||
"marked": "7.0.5",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -194,7 +193,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.8",
|
||||
"del": "7.0.0",
|
||||
"eslint": "8.47.0",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
|
BIN
public/static/images/logo_discord.png
Normal file
BIN
public/static/images/logo_discord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
public/static/images/logo_twitter.png
Normal file
BIN
public/static/images/logo_twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230802.0"
|
||||
version = "20230831.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -35,6 +35,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@property() public oauth2State?: string;
|
||||
|
||||
@property() public translationFragment = "page-authorize";
|
||||
|
||||
@state() private _authProvider?: AuthProvider;
|
||||
|
||||
@state() private _authProviders?: AuthProvider[];
|
||||
@@ -45,7 +47,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.translationFragment = "page-authorize";
|
||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||
if (query.client_id) {
|
||||
this.clientId = query.client_id;
|
||||
@@ -102,7 +103,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
: nothing}
|
||||
|
||||
<ha-auth-flow
|
||||
.resources=${this.resources}
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
|
@@ -107,6 +107,7 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
.value=${this.stepData[schema.name] || ""}
|
||||
.autocomplete=${schema.autocomplete}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
@@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
interface Tooltip extends TooltipModel<any> {
|
||||
export interface ChartResizeOptions {
|
||||
aspectRatio?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Tooltip
|
||||
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
|
||||
top: string;
|
||||
left: string;
|
||||
}
|
||||
|
||||
@customElement("ha-chart-base")
|
||||
export default class HaChartBase extends LitElement {
|
||||
export class HaChartBase extends LitElement {
|
||||
public chart?: Chart;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._releaseCanvas();
|
||||
super.disconnectedCallback();
|
||||
@@ -65,6 +64,36 @@ export default class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public updateChart = (
|
||||
mode:
|
||||
| "resize"
|
||||
| "reset"
|
||||
| "none"
|
||||
| "hide"
|
||||
| "show"
|
||||
| "default"
|
||||
| "active"
|
||||
| undefined
|
||||
): void => {
|
||||
this.chart?.update(mode);
|
||||
};
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
if (options?.aspectRatio && !options.height) {
|
||||
options.height = Math.round(
|
||||
(options.width ?? this.clientWidth) / options.aspectRatio
|
||||
);
|
||||
} else if (options?.aspectRatio && !options.width) {
|
||||
options.width = Math.round(
|
||||
(options.height ?? this.clientHeight) * options.aspectRatio
|
||||
);
|
||||
}
|
||||
this.chart?.resize(
|
||||
options?.width ?? this.clientWidth,
|
||||
options?.height ?? this.clientHeight
|
||||
);
|
||||
};
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setupChart();
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
@@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement {
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("plugins")) {
|
||||
if (changedProps.has("plugins") || changedProps.has("chartType")) {
|
||||
this.chart.destroy();
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("chartType")) {
|
||||
this.chart.config.type = this.chartType;
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (this._hiddenDatasets.size) {
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
@@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement {
|
||||
</div>`
|
||||
: ""}
|
||||
<div
|
||||
class="chartContainer"
|
||||
class="animationContainer"
|
||||
style=${styleMap({
|
||||
height: `${this.height ?? this._chartHeight}px`,
|
||||
height: `${this.height || this._chartHeight || 0}px`,
|
||||
overflow: this._chartHeight ? "initial" : "hidden",
|
||||
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
|
||||
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
|
||||
})}
|
||||
>
|
||||
<canvas></canvas>
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
|
||||
style=${styleMap({
|
||||
top: this._tooltip.top,
|
||||
left: this._tooltip.left,
|
||||
})}
|
||||
>
|
||||
<div class="title">${this._tooltip.title}</div>
|
||||
${this._tooltip.beforeBody
|
||||
? html`<div class="beforeBody">
|
||||
${this._tooltip.beforeBody}
|
||||
</div>`
|
||||
: ""}
|
||||
<div>
|
||||
<ul>
|
||||
${this._tooltip.body.map(
|
||||
(item, i) =>
|
||||
html`<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: this._tooltip!.labelColors[i]
|
||||
.backgroundColor as string,
|
||||
borderColor: this._tooltip!.labelColors[i]
|
||||
.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${item.lines.join("\n")}
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
${this._tooltip.footer.length
|
||||
? html`<div class="footer">
|
||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
<div
|
||||
class="chartContainer"
|
||||
style=${styleMap({
|
||||
height: `${
|
||||
this.height ?? this._chartHeight ?? this.clientWidth / 2
|
||||
}px`,
|
||||
"padding-left": `${
|
||||
computeRTL(this.hass) ? 0 : this.paddingYAxis
|
||||
}px`,
|
||||
"padding-right": `${
|
||||
computeRTL(this.hass) ? this.paddingYAxis : 0
|
||||
}px`,
|
||||
})}
|
||||
>
|
||||
<canvas></canvas>
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chartTooltip ${classMap({
|
||||
[this._tooltip.yAlign]: true,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
top: this._tooltip.top,
|
||||
left: this._tooltip.left,
|
||||
})}
|
||||
>
|
||||
<div class="title">${this._tooltip.title}</div>
|
||||
${this._tooltip.beforeBody
|
||||
? html`<div class="beforeBody">
|
||||
${this._tooltip.beforeBody}
|
||||
</div>`
|
||||
: ""}
|
||||
<div>
|
||||
<ul>
|
||||
${this._tooltip.body.map(
|
||||
(item, i) =>
|
||||
html`<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: this._tooltip!.labelColors[i]
|
||||
.backgroundColor as string,
|
||||
borderColor: this._tooltip!.labelColors[i]
|
||||
.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${item.lines.join("\n")}
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
${this._tooltip.footer.length
|
||||
? html`<div class="footer">
|
||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
private _createOptions() {
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
...this.options,
|
||||
plugins: {
|
||||
...this.options?.plugins,
|
||||
@@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement {
|
||||
return [
|
||||
...(this.plugins || []),
|
||||
{
|
||||
id: "afterRenderHook",
|
||||
afterRender: (chart) => {
|
||||
id: "resizeHook",
|
||||
resize: (chart) => {
|
||||
const change = chart.height - (this._chartHeight ?? 0);
|
||||
if (!this._chartHeight || change > 0 || change < -12) {
|
||||
if (!this._chartHeight || change > 12 || change < -12) {
|
||||
// hysteresis to prevent infinite render loops
|
||||
this._chartHeight = chart.height;
|
||||
}
|
||||
@@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
public updateChart = (
|
||||
mode:
|
||||
| "resize"
|
||||
| "reset"
|
||||
| "none"
|
||||
| "hide"
|
||||
| "show"
|
||||
| "normal"
|
||||
| "active"
|
||||
| undefined
|
||||
): void => {
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.update(mode);
|
||||
this.chart.destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
@@ -310,11 +344,14 @@ export default class HaChartBase extends LitElement {
|
||||
display: block;
|
||||
position: var(--chart-base-position, relative);
|
||||
}
|
||||
.chartContainer {
|
||||
.animationContainer {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.chartContainer {
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -11,14 +11,18 @@ import {
|
||||
} from "../../common/number/format_number";
|
||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import {
|
||||
ChartResizeOptions,
|
||||
HaChartBase,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
} from "./ha-chart-base";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
class StateHistoryChartLine extends LitElement {
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||
@@ -47,6 +51,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -127,12 +137,16 @@ class StateHistoryChartLine extends LitElement {
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
this.hass.states[this.data[context.datasetIndex].entity_id],
|
||||
this.hass.entities[
|
||||
this.data[context.datasetIndex].entity_id
|
||||
]
|
||||
)
|
||||
this.data[context.datasetIndex]?.entity_id
|
||||
? getNumberFormatOptions(
|
||||
this.hass.states[
|
||||
this.data[context.datasetIndex].entity_id
|
||||
],
|
||||
this.hass.entities[
|
||||
this.data[context.datasetIndex].entity_id
|
||||
]
|
||||
)
|
||||
: undefined
|
||||
)} ${this.unit}`,
|
||||
},
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -8,7 +8,11 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import {
|
||||
ChartResizeOptions,
|
||||
HaChartBase,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
} from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
|
||||
@@ -46,6 +50,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
|
@@ -6,7 +6,13 @@ import {
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import {
|
||||
@@ -18,6 +24,9 @@ import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
||||
import { ChartResizeOptions } from "./ha-chart-base";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
@@ -75,6 +84,16 @@ export class StateHistoryCharts extends LitElement {
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@queryAll("state-history-chart-line")
|
||||
private _charts?: StateHistoryChartLine[];
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._charts?.forEach(
|
||||
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
|
||||
chart.resize(options)
|
||||
);
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html`<div class="info">
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@@ -42,7 +43,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
};
|
||||
|
||||
@customElement("statistics-chart")
|
||||
class StatisticsChart extends LitElement {
|
||||
export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public statisticsData?: Statistics;
|
||||
@@ -75,8 +76,14 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
|
||||
private _computedStyle?: CSSStyleDeclaration;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
this._chart?.resize(options);
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return changedProps.size > 1 || !changedProps.has("hass");
|
||||
}
|
||||
|
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
BarControllerChartOptions,
|
||||
BarControllerDatasetOptions,
|
||||
} from "chart.js";
|
||||
|
||||
export interface TimeLineData {
|
||||
start: Date;
|
||||
end: Date;
|
||||
|
@@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
|
||||
export class TextBarElement extends BarElement {
|
||||
static id = "textbar";
|
||||
|
||||
draw(ctx) {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
super.draw(ctx);
|
||||
const options = this.options as TextBaroptions;
|
||||
const { x, y, base, width, text } = (
|
||||
|
@@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js";
|
||||
import { TimeLineData } from "./const";
|
||||
import { TextBarProps } from "./textbar-element";
|
||||
|
||||
function borderProps(properties) {
|
||||
let reverse;
|
||||
let start;
|
||||
let end;
|
||||
let top;
|
||||
let bottom;
|
||||
if (properties.horizontal) {
|
||||
reverse = properties.base > properties.x;
|
||||
start = "left";
|
||||
end = "right";
|
||||
} else {
|
||||
reverse = properties.base < properties.y;
|
||||
start = "bottom";
|
||||
end = "top";
|
||||
}
|
||||
if (reverse) {
|
||||
top = "end";
|
||||
bottom = "start";
|
||||
} else {
|
||||
top = "start";
|
||||
bottom = "end";
|
||||
}
|
||||
return { start, end, reverse, top, bottom };
|
||||
}
|
||||
|
||||
function setBorderSkipped(properties, options, stack, index) {
|
||||
let edge = options.borderSkipped;
|
||||
const res = {};
|
||||
|
||||
if (!edge) {
|
||||
properties.borderSkipped = res;
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge === true) {
|
||||
properties.borderSkipped = {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, end, reverse, top, bottom } = borderProps(properties);
|
||||
|
||||
if (edge === "middle" && stack) {
|
||||
properties.enableBorderRadius = true;
|
||||
if ((stack._top || 0) === index) {
|
||||
edge = top;
|
||||
} else if ((stack._bottom || 0) === index) {
|
||||
edge = bottom;
|
||||
} else {
|
||||
res[parseEdge(bottom, start, end, reverse)] = true;
|
||||
edge = top;
|
||||
}
|
||||
}
|
||||
|
||||
res[parseEdge(edge, start, end, reverse)] = true;
|
||||
properties.borderSkipped = res;
|
||||
}
|
||||
|
||||
function parseEdge(edge, a, b, reverse) {
|
||||
if (reverse) {
|
||||
edge = swap(edge, a, b);
|
||||
edge = startEnd(edge, b, a);
|
||||
} else {
|
||||
edge = startEnd(edge, a, b);
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
function swap(orig, v1, v2) {
|
||||
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
||||
}
|
||||
|
||||
function startEnd(v, start, end) {
|
||||
return v === "start" ? start : v === "end" ? end : v;
|
||||
}
|
||||
|
||||
function setInflateAmount(
|
||||
properties,
|
||||
{ inflateAmount }: { inflateAmount?: string | number },
|
||||
ratio
|
||||
) {
|
||||
properties.inflateAmount =
|
||||
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
|
||||
}
|
||||
|
||||
function parseValue(entry, item, vScale, i) {
|
||||
const startValue = vScale.parse(entry.start, i);
|
||||
const endValue = vScale.parse(entry.end, i);
|
||||
@@ -97,7 +186,7 @@ export class TimelineController extends BarController {
|
||||
bars: BarElement[],
|
||||
start: number,
|
||||
count: number,
|
||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
|
||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
|
||||
) {
|
||||
const vScale = this._cachedMeta.vScale!;
|
||||
const iScale = this._cachedMeta.iScale!;
|
||||
@@ -114,15 +203,15 @@ export class TimelineController extends BarController {
|
||||
for (let index = start; index < start + count; index++) {
|
||||
const data = dataset.data[index] as TimeLineData;
|
||||
|
||||
// @ts-ignore
|
||||
const y = vScale.getPixelForValue(this.index);
|
||||
|
||||
// @ts-ignore
|
||||
const xStart = iScale.getPixelForValue(data.start.getTime());
|
||||
// @ts-ignore
|
||||
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||
const width = xEnd - xStart;
|
||||
|
||||
const parsed = this.getParsed(index);
|
||||
const stack = (parsed._stacks || {})[vScale.axis];
|
||||
|
||||
const height = 10;
|
||||
|
||||
const properties: TextBarProps = {
|
||||
@@ -145,7 +234,10 @@ export class TimelineController extends BarController {
|
||||
backgroundColor: data.color,
|
||||
};
|
||||
}
|
||||
const options = properties.options || bars[index].options;
|
||||
|
||||
setBorderSkipped(properties, options, stack, index);
|
||||
setInflateAmount(properties, options, 1);
|
||||
this.updateElement(bars[index], index, properties as any, mode);
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-attribute-value";
|
||||
import "./ha-expansion-panel";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@@ -18,16 +25,30 @@ class HaAttributes extends LitElement {
|
||||
|
||||
@state() private _expanded = false;
|
||||
|
||||
private get _filteredAttributes() {
|
||||
return this.computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("extraFilters") ||
|
||||
changedProperties.has("stateObj")
|
||||
) {
|
||||
this.toggleAttribute("empty", this._filteredAttributes.length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const attributes = this.computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
);
|
||||
const attributes = this._filteredAttributes;
|
||||
|
||||
if (attributes.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
@@ -10,12 +10,12 @@ import "./ha-icon-button";
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<div class="header_title">${title}</div>
|
||||
<ha-icon-button
|
||||
.label=${hass.localize("ui.dialogs.generic.close")}
|
||||
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"file-picked": { files: FileList };
|
||||
"file-picked": { files: File[] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +25,22 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public label!: string;
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value: string | TemplateResult | null = null;
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property() public value?: File | File[] | FileList | string;
|
||||
|
||||
@property({ type: Boolean }) private multiple = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
|
||||
|
||||
@property({ type: Boolean }) private uploading = false;
|
||||
|
||||
@property({ type: Number }) private progress?: number;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
||||
private autoOpenFileDialog = false;
|
||||
|
||||
@@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<ha-circular-progress
|
||||
alt="Uploading"
|
||||
size="large"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
<label
|
||||
for="input"
|
||||
class="mdc-text-field mdc-text-field--filled ${classMap({
|
||||
"mdc-text-field--focused": this._drag,
|
||||
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
||||
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>
|
||||
<span class="mdc-text-field__ripple"></span>
|
||||
<span
|
||||
class="mdc-floating-label ${this.value || this._drag
|
||||
? "mdc-floating-label--float-above"
|
||||
: ""}"
|
||||
id="label"
|
||||
>${this.label}</span
|
||||
? html`<div class="container">
|
||||
<div class="row">
|
||||
<span class="header"
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this.value }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>
|
||||
${this.icon
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._openFilePicker}
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
${this.progress
|
||||
? html`<span class="progress"
|
||||
>${this.progress}${blankBeforePercent(
|
||||
this.hass!.locale
|
||||
)}%</span
|
||||
>`
|
||||
: ""}
|
||||
<div class="value">${this.value}</div>
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="mdc-text-field__input file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
aria-labelledby="label"
|
||||
/>
|
||||
${this.value
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||
</div>
|
||||
<mwc-linear-progress
|
||||
.indeterminate=${!this.progress}
|
||||
.progress=${this.progress ? this.progress / 100 : undefined}
|
||||
></mwc-linear-progress>
|
||||
</div>`
|
||||
: html`<label
|
||||
for=${this.value ? "" : "input"}
|
||||
class="container ${classMap({
|
||||
dragged: this._drag,
|
||||
multiple: this.multiple,
|
||||
value: Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>${!this.value
|
||||
? html`<ha-svg-icon
|
||||
class="big-icon"
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<span
|
||||
class="mdc-line-ripple ${this._drag
|
||||
? "mdc-line-ripple--active"
|
||||
: ""}"
|
||||
></span>
|
||||
</label>
|
||||
`}
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
? html`<div class="row">
|
||||
<div class="value" @click=${this._openFilePicker}>
|
||||
<ha-svg-icon
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
${this.value}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: (this.value instanceof FileList
|
||||
? Array.from(this.value)
|
||||
: ensureArray(this.value)
|
||||
).map(
|
||||
(file) =>
|
||||
html`<div class="row">
|
||||
<div class="value" @click=${this._openFilePicker}>
|
||||
<ha-svg-icon
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
${file.name} - ${bytesToString(file.size)}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
.accept=${this.accept}
|
||||
.multiple=${this.multiple}
|
||||
@change=${this._handleFilePicked}
|
||||
/></label>`}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.dataTransfer?.files) {
|
||||
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
|
||||
fireEvent(this, "file-picked", {
|
||||
files:
|
||||
this.multiple || ev.dataTransfer.files.length === 1
|
||||
? Array.from(ev.dataTransfer.files)
|
||||
: [ev.dataTransfer.files[0]],
|
||||
});
|
||||
}
|
||||
this._drag = false;
|
||||
}
|
||||
@@ -140,93 +188,121 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
private _handleFilePicked(ev) {
|
||||
if (ev.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.value = ev.target.files;
|
||||
fireEvent(this, "file-picked", { files: ev.target.files });
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = null;
|
||||
this._input!.value = "";
|
||||
this.value = undefined;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.mdc-text-field--filled {
|
||||
height: auto;
|
||||
padding-top: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
||||
padding-top: 28px;
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
||||
.mdc-text-field__icon {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label {
|
||||
inset-inline-start: 48px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field__icon--trailing {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 240px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: solid 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([disabled]) .container {
|
||||
border-color: var(--disabled-color);
|
||||
}
|
||||
label.dragged {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.dragged:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary-color);
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
}
|
||||
label.value {
|
||||
cursor: default;
|
||||
}
|
||||
label.value.multiple {
|
||||
justify-content: unset;
|
||||
overflow: auto;
|
||||
}
|
||||
.highlight {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-button {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.supports {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
:host([disabled]) .secondary {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
.value {
|
||||
cursor: pointer;
|
||||
}
|
||||
.value ha-svg-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.big-icon {
|
||||
--mdc-icon-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-button {
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
--mdc-icon-button-size: 24px;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
font-weight: 500;
|
||||
}
|
||||
.progress {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -68,6 +68,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
${isPassword
|
||||
? html`<ha-icon-button
|
||||
|
@@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import "../resources/intl-polyfill";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
@@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property() public languages?: string[];
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@@ -62,8 +63,8 @@ export class HaLanguagePicker extends LitElement {
|
||||
}
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
this.nativeName,
|
||||
this.hass?.locale
|
||||
);
|
||||
const selectedItemIndex = languageOptions.findIndex(
|
||||
(option) => option.value === this.value
|
||||
@@ -78,11 +79,11 @@ export class HaLanguagePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _getLanguagesOptions = memoizeOne(
|
||||
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
|
||||
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
|
||||
let options: { label: string; value: string }[] = [];
|
||||
|
||||
if (nativeName) {
|
||||
const translations = this.hass.translationMetadata.translations;
|
||||
const translations = translationMetadata.translations;
|
||||
options = languages.map((lang) => {
|
||||
let label = translations[lang]?.nativeName;
|
||||
if (!label) {
|
||||
@@ -101,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
|
||||
label,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
} else if (locale) {
|
||||
options = languages.map((lang) => ({
|
||||
value: lang,
|
||||
label: formatLanguageCode(lang, locale),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.noSort) {
|
||||
if (!this.noSort && locale) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
||||
);
|
||||
@@ -118,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
|
||||
);
|
||||
|
||||
private _computeDefaultLanguageOptions() {
|
||||
if (!this.hass.translationMetadata?.translations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._defaultLanguages = Object.keys(
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
this.nativeName,
|
||||
this.hass?.locale
|
||||
);
|
||||
|
||||
const value =
|
||||
@@ -139,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.language-picker.language")}
|
||||
.value=${value}
|
||||
.label=${this.label ??
|
||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||
"Language")}
|
||||
.value=${value || ""}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@@ -151,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
>
|
||||
${languageOptions.length === 0
|
||||
? html`<ha-list-item value=""
|
||||
>${this.hass.localize(
|
||||
>${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
)}</ha-list-item
|
||||
) || "No languages"}</ha-list-item
|
||||
>`
|
||||
: languageOptions.map(
|
||||
(option) => html`
|
||||
@@ -176,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (!this.hass || target.value === "" || target.value === this.value) {
|
||||
if (target.value === "" || target.value === this.value) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { mdiImagePlus } from "@mdi/js";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
showImageCropperDialog,
|
||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-file-upload";
|
||||
|
||||
@@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property() public currentImageAltText?: string;
|
||||
|
||||
@property({ type: Boolean }) public crop = false;
|
||||
|
||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||
@@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
|
||||
@state() private _uploading = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
.uploading=${this._uploading}
|
||||
.value=${this.value ? html`<img .src=${this.value} />` : ""}
|
||||
@file-picked=${this._handleFilePicked}
|
||||
@change=${this._handleFileCleared}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
></ha-file-upload>
|
||||
`;
|
||||
if (!this.value) {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
.secondary=${this.secondary}
|
||||
.supports=${this.supports ||
|
||||
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
||||
.uploading=${this._uploading}
|
||||
@file-picked=${this._handleFilePicked}
|
||||
@change=${this._handleFileCleared}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
return html`<div class="center-vertical">
|
||||
<div class="value">
|
||||
<img
|
||||
.src=${this.value}
|
||||
alt=${this.currentImageAltText ||
|
||||
this.hass.localize("ui.components.picture-upload.current_image_alt")}
|
||||
/>
|
||||
<ha-button
|
||||
@click=${this._handleChangeClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.picture-upload.change_picture"
|
||||
)}
|
||||
>
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _handleChangeClick() {
|
||||
this.value = null;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private async _handleFilePicked(ev) {
|
||||
@@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 240px;
|
||||
}
|
||||
ha-file-upload {
|
||||
height: 100%;
|
||||
}
|
||||
.center-vertical {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.value {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: var(--file-upload-image-border-radius);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
.mdc-select--filled .mdc-select__anchor {
|
||||
height: var(--ha-select-height, 56px);
|
||||
}
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: 12px;
|
||||
inset-inline-end: initial;
|
||||
|
@@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.supports=${this.helper}
|
||||
.uploading=${this._busy}
|
||||
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
|
||||
.value=${this.value
|
||||
? this._filename?.name ||
|
||||
this.hass.localize("ui.components.selectors.file.unknown_file")
|
||||
: undefined}
|
||||
@file-picked=${this._uploadFile}
|
||||
@change=${this._removeFile}
|
||||
></ha-file-upload>
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const createLanguageListEl = (hass: HomeAssistant) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "languages";
|
||||
for (const [language, metadata] of Object.entries(
|
||||
hass.translationMetadata.translations
|
||||
)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = language;
|
||||
option.innerText = metadata.nativeName;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
};
|
@@ -649,11 +649,11 @@ export const getDefaultForecastType = (stateObj: HassEntityBase) => {
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
||||
return "daily";
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
||||
return "hourly";
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
||||
return "twice_daily";
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
||||
return "hourly";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface RenderTemplateResult {
|
||||
result: string;
|
||||
@@ -12,6 +13,17 @@ interface TemplateListeners {
|
||||
time: boolean;
|
||||
}
|
||||
|
||||
export type TemplatePreview = TemplatePreviewState | TemplatePreviewError;
|
||||
|
||||
interface TemplatePreviewState {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
interface TemplatePreviewError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const subscribeRenderTemplate = (
|
||||
conn: Connection,
|
||||
onChange: (result: RenderTemplateResult) => void,
|
||||
@@ -27,3 +39,17 @@ export const subscribeRenderTemplate = (
|
||||
type: "render_template",
|
||||
...params,
|
||||
});
|
||||
|
||||
export const subscribePreviewTemplate = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: TemplatePreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "template/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
||||
|
@@ -404,8 +404,6 @@ export interface RequestedGrant {
|
||||
clientSideAuth: boolean;
|
||||
}
|
||||
|
||||
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
|
||||
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_or_entry_id: {
|
||||
|
114
src/dialogs/config-flow/previews/flow-preview-template.ts
Normal file
114
src/dialogs/config-flow/previews/flow-preview-template.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
TemplatePreview,
|
||||
subscribePreviewTemplate,
|
||||
} from "../../../data/ws-templates";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("flow-preview-template")
|
||||
class FlowPreviewTemplate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property() public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: TemplatePreview) => {
|
||||
if ("error" in preview) {
|
||||
this._error = preview.error;
|
||||
this._preview = undefined;
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.flow_preview`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
};
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewTemplate(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||
} catch (err: any) {
|
||||
if (typeof err.message === "string") {
|
||||
this._error = err.message;
|
||||
} else {
|
||||
this._error = undefined;
|
||||
fireEvent(this, "set-flow-errors", err.message);
|
||||
}
|
||||
this._unsub = undefined;
|
||||
this._preview = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-template": FlowPreviewTemplate;
|
||||
}
|
||||
}
|
@@ -70,7 +70,7 @@ class StepFlowForm extends LitElement {
|
||||
></ha-form>
|
||||
</div>
|
||||
${step.preview
|
||||
? html`<div class="preview">
|
||||
? html`<div class="preview" @set-flow-errors=${this._setError}>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.preview"
|
||||
@@ -107,6 +107,10 @@ class StepFlowForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _setError(ev: CustomEvent) {
|
||||
this.step = { ...this.step, errors: ev.detail };
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
|
||||
@@ -253,6 +257,9 @@ class StepFlowForm extends LitElement {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"set-flow-errors": { errors: DataEntryFlowStepForm["errors"] };
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
"step-flow-form": StepFlowForm;
|
||||
}
|
||||
|
@@ -280,15 +280,21 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const activeModes = this.stateObj.attributes.hvac_modes.filter(
|
||||
(m) => m !== "off"
|
||||
);
|
||||
|
||||
if (
|
||||
supportsTargetTemperature &&
|
||||
this._targetTemperature.value != null &&
|
||||
this.stateObj.state !== UNAVAILABLE
|
||||
) {
|
||||
const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) =>
|
||||
["heat", "cool", "heat_cool"].includes(m)
|
||||
);
|
||||
const sliderMode =
|
||||
SLIDER_MODES[
|
||||
heatCoolModes.length === 1 && ["off", "auto"].includes(mode)
|
||||
? heatCoolModes[0]
|
||||
: mode
|
||||
];
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="container"
|
||||
@@ -299,9 +305,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
>
|
||||
<ha-control-circular-slider
|
||||
.inactive=${!active}
|
||||
.mode=${mode === "off" && activeModes.length === 1
|
||||
? SLIDER_MODES[activeModes[0]]
|
||||
: SLIDER_MODES[mode]}
|
||||
.mode=${sliderMode}
|
||||
.value=${this._targetTemperature.value}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
|
@@ -34,6 +34,10 @@ export const moreInfoControlStyle = css`
|
||||
}
|
||||
|
||||
ha-attributes {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-more-info-control-select-container + ha-attributes:not([empty]) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
@@ -142,7 +142,7 @@ class MoreInfoClimate extends LitElement {
|
||||
.selected=${this._mainControl === "temperature"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.color"
|
||||
"ui.dialogs.more_info_control.climate.temperature"
|
||||
)}
|
||||
.control=${"temperature"}
|
||||
@click=${this._setMainControl}
|
||||
@@ -153,7 +153,7 @@ class MoreInfoClimate extends LitElement {
|
||||
.selected=${this._mainControl === "humidity"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.color_temp"
|
||||
"ui.dialogs.more_info_control.climate.humidity"
|
||||
)}
|
||||
.control=${"humidity"}
|
||||
@click=${this._setMainControl}
|
||||
@@ -166,10 +166,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
<ha-more-info-control-select-container>
|
||||
<ha-control-select-menu
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"hvac_mode"
|
||||
)}
|
||||
.label=${this.hass.localize("ui.card.climate.mode")}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
|
@@ -86,7 +86,7 @@ class MoreInfoFan extends LitElement {
|
||||
}
|
||||
|
||||
_handleOscillating(ev) {
|
||||
const newVal = ev.target.value === "on";
|
||||
const newVal = ev.target.value === "true";
|
||||
|
||||
this.hass.callService("fan", "oscillate", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
@@ -269,7 +269,9 @@ class MoreInfoFan extends LitElement {
|
||||
this.stateObj,
|
||||
"oscillating"
|
||||
)}
|
||||
.value=${this.stateObj.attributes.oscillating ? "on" : "off"}
|
||||
.value=${this.stateObj.attributes.oscillating
|
||||
? "true"
|
||||
: "false"}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@@ -280,19 +282,27 @@ class MoreInfoFan extends LitElement {
|
||||
slot="icon"
|
||||
.path=${haOscillatingOff}
|
||||
></ha-svg-icon>
|
||||
<ha-list-item value="on" graphic="icon">
|
||||
<ha-list-item value="true" graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${haOscillating}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("state.default.on")}
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"oscillating",
|
||||
true
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="off" graphic="icon">
|
||||
<ha-list-item value="false" graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${haOscillatingOff}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("state.default.off")}
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"oscillating",
|
||||
false
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
|
@@ -73,10 +73,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
${supportOperationMode && stateObj.attributes.operation_list
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"operation"
|
||||
)}
|
||||
.label=${this.hass.localize("ui.card.water_heater.mode")}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
@@ -122,11 +119,19 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
slot="graphic"
|
||||
.path=${mdiAccountArrowRight}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("state.default.on")}
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"away_mode",
|
||||
"on"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="off" graphic="icon">
|
||||
<ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon>
|
||||
${this.hass.localize("state.default.off")}
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"away_mode",
|
||||
"off"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
|
@@ -252,7 +252,8 @@ class MoreInfoWeather extends LitElement {
|
||||
${this._showValue(item.templow)
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"templow"
|
||||
"templow",
|
||||
item.templow
|
||||
)
|
||||
: hourly
|
||||
? ""
|
||||
@@ -262,7 +263,8 @@ class MoreInfoWeather extends LitElement {
|
||||
${this._showValue(item.temperature)
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"temperature"
|
||||
"temperature",
|
||||
item.temperature
|
||||
)
|
||||
: "—"}
|
||||
</div>
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
mdiPencilOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -38,15 +38,17 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../state-summary/state-card-content";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
computeShowHistoryComponent,
|
||||
computeShowLogBookComponent,
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
EDITABLE_DOMAINS_WITH_ID,
|
||||
EDITABLE_DOMAINS_WITH_UNIQUE_ID,
|
||||
computeShowHistoryComponent,
|
||||
computeShowLogBookComponent,
|
||||
} from "./const";
|
||||
import "./controls/more-info-default";
|
||||
import "./ha-more-info-history-and-logbook";
|
||||
import type { MoreInfoHistoryAndLogbook } from "./ha-more-info-history-and-logbook";
|
||||
import "./ha-more-info-info";
|
||||
import type { MoreInfoInfo } from "./ha-more-info-info";
|
||||
import "./ha-more-info-settings";
|
||||
import "./more-info-content";
|
||||
|
||||
@@ -91,6 +93,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _infoEditMode = false;
|
||||
|
||||
@query("ha-more-info-info, ha-more-info-history-and-logbook")
|
||||
private _history?: MoreInfoInfo | MoreInfoHistoryAndLogbook;
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
this._entityId = params.entityId;
|
||||
if (!this._entityId) {
|
||||
@@ -263,6 +268,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._handleOpened}
|
||||
.heading=${title}
|
||||
hideActions
|
||||
flexContent
|
||||
@@ -485,6 +491,10 @@ export class MoreInfoDialog extends LitElement {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._history?.resize({ aspectRatio: 2 });
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleDialog,
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
computeShowHistoryComponent,
|
||||
computeShowLogBookComponent,
|
||||
} from "./const";
|
||||
import "./ha-more-info-history";
|
||||
import type { MoreInfoHistory } from "./ha-more-info-history";
|
||||
import "./ha-more-info-logbook";
|
||||
|
||||
@customElement("ha-more-info-history-and-logbook")
|
||||
@@ -14,6 +16,13 @@ export class MoreInfoHistoryAndLogbook extends LitElement {
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@query("ha-more-info-history")
|
||||
private _history?: MoreInfoHistory;
|
||||
|
||||
public resize(options?: ChartResizeOptions) {
|
||||
this._history?.resize(options);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${computeShowHistoryComponent(this.hass, this.entityId)
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { startOfYesterday, subHours } from "date-fns/esm";
|
||||
import { css, html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { createSearchParam } from "../../common/url/search-params";
|
||||
import "../../components/chart/state-history-charts";
|
||||
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
|
||||
import "../../components/chart/statistics-chart";
|
||||
import {
|
||||
computeHistory,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
StatisticsTypes,
|
||||
} from "../../data/recorder";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { StatisticsChart } from "../../components/chart/statistics-chart";
|
||||
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -51,12 +54,22 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
private _metadata?: Record<string, StatisticsMetaData>;
|
||||
|
||||
@query("statistics-chart, state-history-charts") private _chart?:
|
||||
| StateHistoryCharts
|
||||
| StatisticsChart;
|
||||
|
||||
public resize = (options?: ChartResizeOptions): void => {
|
||||
if (this._chart) {
|
||||
this._chart.resize(options);
|
||||
}
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this.entityId) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html` ${isComponentLoaded(this.hass, "history")
|
||||
return html`${isComponentLoaded(this.hass, "history")
|
||||
? html`<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
|
||||
import { ExtEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
} from "./const";
|
||||
import "./ha-more-info-history";
|
||||
import type { MoreInfoHistory } from "./ha-more-info-history";
|
||||
import "./ha-more-info-logbook";
|
||||
import "./more-info-content";
|
||||
|
||||
@@ -25,6 +27,13 @@ export class MoreInfoInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@query("ha-more-info-history")
|
||||
private _history?: MoreInfoHistory;
|
||||
|
||||
public resize(options?: ChartResizeOptions) {
|
||||
this._history?.resize(options);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const entityId = this.entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
|
@@ -6,57 +6,40 @@
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
background-color: #0277bd !important;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
color: var(--primary-text-color, #e1e1e1);
|
||||
}
|
||||
}
|
||||
body {
|
||||
height: auto;
|
||||
padding: 64px 0;
|
||||
padding: 32px 0;
|
||||
}
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
padding: 20px 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
max-width: 432px;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
box-shadow: var(
|
||||
--ha-card-box-shadow,
|
||||
rgba(0, 0, 0, 0.25) 0px 54px 55px,
|
||||
rgba(0, 0, 0, 0.12) 0px -12px 30px,
|
||||
rgba(0, 0, 0, 0.12) 0px 4px 6px,
|
||||
rgba(0, 0, 0, 0.17) 0px 12px 13px,
|
||||
rgba(0, 0, 0, 0.09) 0px -3px 5px
|
||||
);
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
color: var(--primary-text-color, #212121);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 1.96em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 300;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header img {
|
||||
margin-right: 16px;
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
@media (max-width: 592px) {
|
||||
.content {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -64,8 +47,7 @@
|
||||
<body id="particles">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
|
||||
Home Assistant
|
||||
<img src="/static/icons/favicon-192x192.png" alt="Home Assistant" />
|
||||
</div>
|
||||
<ha-onboarding></ha-onboarding>
|
||||
</div>
|
||||
|
@@ -1,103 +0,0 @@
|
||||
/* eslint-plugin-disable lit */
|
||||
/**
|
||||
@license
|
||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
/*
|
||||
This code is copied from app-header-layout.
|
||||
'fullbleed' support is removed as Home Assisstant doesn't use it.
|
||||
transform: translate(0) is added.
|
||||
*/
|
||||
/*
|
||||
FIXME(polymer-modulizer): the above comments were extracted
|
||||
from HTML and may be out of place here. Review them and
|
||||
then delete this comment!
|
||||
*/
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import "@polymer/polymer/polymer-element";
|
||||
|
||||
class HaAppLayout extends customElements.get("app-header-layout") {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
/**
|
||||
* Force app-header-layout to have its own stacking context so that its parent can
|
||||
* control the stacking of it relative to other elements (e.g. app-drawer-layout).
|
||||
* This could be done using \`isolation: isolate\`, but that's not well supported
|
||||
* across browsers.
|
||||
*/
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#wrapper ::slotted([slot="header"]) {
|
||||
@apply --layout-fixed-top;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#wrapper.initializing ::slotted([slot="header"]) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([has-scrolling-region]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([has-scrolling-region]) #wrapper ::slotted([slot="header"]) {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:host([has-scrolling-region])
|
||||
#wrapper.initializing
|
||||
::slotted([slot="header"]) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([has-scrolling-region]) #wrapper #contentContainer {
|
||||
@apply --layout-fit;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:host([has-scrolling-region]) #wrapper.initializing #contentContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#contentContainer {
|
||||
/* Create a stacking context here so that all children appear below the header. */
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
/* Using 'transform' will cause 'position: fixed' elements to behave like
|
||||
'position: absolute' relative to this element. */
|
||||
transform: translate(0);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
@media print {
|
||||
:host([has-scrolling-region]) #wrapper #contentContainer {
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrapper" class="initializing">
|
||||
<slot id="headerSlot" name="header"></slot>
|
||||
|
||||
<div id="contentContainer"><slot></slot></div>
|
||||
<slot id="fab" name="fab"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-app-layout", HaAppLayout);
|
@@ -1,5 +1,5 @@
|
||||
import { LitElement, PropertyValues } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
|
||||
import { Constructor, Resources } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
@@ -15,13 +15,13 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
// Initialized to empty will prevent undefined errors if called before connected to DOM.
|
||||
@property() public localize: LocalizeFunc = empty;
|
||||
|
||||
@property() public resources?: Resources;
|
||||
|
||||
// Use browser language setup before login.
|
||||
@property() public language?: string = getLocalLanguage();
|
||||
|
||||
@property() public translationFragment?: string;
|
||||
|
||||
@state() private _resources?: Resources;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._initializeLocalizeLite();
|
||||
@@ -35,22 +35,27 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.get("language")) {
|
||||
this._resources = undefined;
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
if (changedProperties.get("translationFragment")) {
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
if (
|
||||
this.language &&
|
||||
this.resources &&
|
||||
this._resources &&
|
||||
(changedProperties.has("language") ||
|
||||
changedProperties.has("resources"))
|
||||
changedProperties.has("_resources"))
|
||||
) {
|
||||
computeLocalize(
|
||||
this.constructor.prototype,
|
||||
this.language,
|
||||
this.resources
|
||||
this._resources
|
||||
).then((localize) => {
|
||||
this.localize = localize;
|
||||
});
|
||||
@@ -58,7 +63,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
}
|
||||
|
||||
protected async _initializeLocalizeLite() {
|
||||
if (this.resources) {
|
||||
if (this._resources) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +73,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
if (__DEV__) {
|
||||
setTimeout(
|
||||
() =>
|
||||
!this.resources &&
|
||||
!this._resources &&
|
||||
// eslint-disable-next-line
|
||||
console.error(
|
||||
"Forgot to pass in resources or set translationFragment for",
|
||||
@@ -84,7 +89,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
this.translationFragment!,
|
||||
this.language!
|
||||
);
|
||||
this.resources = {
|
||||
this._resources = {
|
||||
[this.language!]: data,
|
||||
};
|
||||
}
|
||||
|
@@ -1,85 +0,0 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../components/ha-svg-icon";
|
||||
|
||||
@customElement("action-badge")
|
||||
class ActionBadge extends LitElement {
|
||||
@property() public icon!: string;
|
||||
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public badgeIcon?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clickable = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${this.icon}></ha-svg-icon>
|
||||
${this.badgeIcon
|
||||
? html`<ha-svg-icon
|
||||
class="badge"
|
||||
.path=${this.badgeIcon}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${this.title}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
:host([clickable]) {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto 8px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--secondary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:host([clickable]) .icon {
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
color: var(--primary-color);
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
width: 18px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
min-height: 2.3em;
|
||||
word-break: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"action-badge": ActionBadge;
|
||||
}
|
||||
}
|
6759
src/onboarding/dialogs/app-dialog.ts
Normal file
6759
src/onboarding/dialogs/app-dialog.ts
Normal file
File diff suppressed because one or more lines are too long
106
src/onboarding/dialogs/community-dialog.ts
Normal file
106
src/onboarding/dialogs/community-dialog.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-list-item";
|
||||
|
||||
@customElement("community-dialog")
|
||||
class DialogCommunity extends LitElement {
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
public async showDialog(params): Promise<void> {
|
||||
this.localize = params.localize;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this.localize = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.localize) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
undefined,
|
||||
this.localize("ui.panel.page-onboarding.welcome.community")
|
||||
)}
|
||||
>
|
||||
<mwc-list>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://community.home-assistant.io/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://www.home-assistant.io/newsletter/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.welcome.open_home_newsletter"
|
||||
)}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://www.home-assistant.io/join-chat"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/images/logo_discord.png" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://twitter.com/home_assistant"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/images/logo_twitter.png" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.twitter")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
</mwc-list>
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(400px, 90vw);
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-list-item {
|
||||
height: 56px;
|
||||
--mdc-list-item-meta-size: 20px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"community-dialog": DialogCommunity;
|
||||
}
|
||||
}
|
15
src/onboarding/dialogs/show-app-dialog.ts
Normal file
15
src/onboarding/dialogs/show-app-dialog.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
export const loadAppDialog = () => import("./app-dialog");
|
||||
|
||||
export const showAppDialog = (
|
||||
element: HTMLElement,
|
||||
params: { localize: LocalizeFunc }
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "app-dialog",
|
||||
dialogImport: loadAppDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
15
src/onboarding/dialogs/show-community-dialog.ts
Normal file
15
src/onboarding/dialogs/show-community-dialog.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
export const loadCommunityDialog = () => import("./community-dialog");
|
||||
|
||||
export const showCommunityDialog = (
|
||||
element: HTMLElement,
|
||||
params: { localize: LocalizeFunc }
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "community-dialog",
|
||||
dialogImport: loadCommunityDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
Auth,
|
||||
createConnection,
|
||||
@@ -5,35 +6,45 @@ import {
|
||||
getAuth,
|
||||
subscribeConfig,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { html, PropertyValues, nothing } from "lit";
|
||||
import { PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
enableWrite,
|
||||
loadTokens,
|
||||
saveTokens,
|
||||
} from "../common/auth/token_storage";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-language-picker";
|
||||
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
|
||||
import {
|
||||
fetchInstallationType,
|
||||
fetchOnboardingOverview,
|
||||
OnboardingResponses,
|
||||
OnboardingStep,
|
||||
fetchInstallationType,
|
||||
fetchOnboardingOverview,
|
||||
onboardIntegrationStep,
|
||||
} from "../data/onboarding";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./onboarding-analytics";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import {
|
||||
enableWrite,
|
||||
loadTokens,
|
||||
saveTokens,
|
||||
} from "../common/auth/token_storage";
|
||||
import "./onboarding-welcome";
|
||||
import "./onboarding-welcome-links";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
|
||||
type OnboardingEvent =
|
||||
| {
|
||||
type: "init";
|
||||
result: { restore: boolean };
|
||||
}
|
||||
| {
|
||||
type: "user";
|
||||
result: OnboardingResponses["user"];
|
||||
@@ -49,13 +60,21 @@ type OnboardingEvent =
|
||||
type: "analytics";
|
||||
};
|
||||
|
||||
interface OnboardingProgressEvent {
|
||||
increase?: number;
|
||||
decrease?: number;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"onboarding-step": OnboardingEvent;
|
||||
"onboarding-progress": OnboardingProgressEvent;
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"onboarding-step": HASSDomEvent<OnboardingEvent>;
|
||||
"onboarding-progress": HASSDomEvent<OnboardingProgressEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
@property() public translationFragment = "page-onboarding";
|
||||
|
||||
@state() private _progress = 0;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _init = false;
|
||||
|
||||
@state() private _restoring = false;
|
||||
|
||||
@state() private _supervisor?: boolean;
|
||||
@@ -74,29 +97,58 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
@state() private _steps?: OnboardingStep[];
|
||||
|
||||
protected render() {
|
||||
return html`<mwc-linear-progress
|
||||
.progress=${this._progress}
|
||||
></mwc-linear-progress>
|
||||
<ha-card>
|
||||
<div class="card-content">${this._renderStep()}</div>
|
||||
</ha-card>
|
||||
${this._init
|
||||
? html`<onboarding-welcome-links
|
||||
.localize=${this.localize}
|
||||
></onboarding-welcome-links>`
|
||||
: nothing}
|
||||
<div class="footer">
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
nativeName
|
||||
@value-changed=${this._languageChanged}
|
||||
></ha-language-picker>
|
||||
<a
|
||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-onboarding.help")}</a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderStep() {
|
||||
if (this._init) {
|
||||
return html`<onboarding-welcome
|
||||
.localize=${this.localize}
|
||||
.language=${this.language}
|
||||
.supervisor=${this._supervisor}
|
||||
></onboarding-welcome>`;
|
||||
}
|
||||
|
||||
if (this._restoring) {
|
||||
return html`<onboarding-restore-backup .localize=${this.localize}>
|
||||
</onboarding-restore-backup>`;
|
||||
}
|
||||
|
||||
const step = this._curStep()!;
|
||||
|
||||
if (this._loading || !step) {
|
||||
return html`<onboarding-loading></onboarding-loading> `;
|
||||
}
|
||||
if (step.step === "user") {
|
||||
return html`
|
||||
${!this._restoring
|
||||
? html`<onboarding-create-user
|
||||
.localize=${this.localize}
|
||||
.language=${this.language}
|
||||
>
|
||||
</onboarding-create-user>`
|
||||
: ""}
|
||||
${this._supervisor
|
||||
? html`<onboarding-restore-backup
|
||||
.localize=${this.localize}
|
||||
.restoring=${this._restoring}
|
||||
@restoring=${this._restoringBackup}
|
||||
>
|
||||
</onboarding-restore-backup>`
|
||||
: ""}
|
||||
`;
|
||||
return html`<onboarding-create-user
|
||||
.localize=${this.localize}
|
||||
.language=${this.language}
|
||||
>
|
||||
</onboarding-create-user>`;
|
||||
}
|
||||
if (step.step === "core_config") {
|
||||
return html`
|
||||
@@ -114,7 +166,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
></onboarding-analytics>
|
||||
`;
|
||||
}
|
||||
|
||||
if (step.step === "integration") {
|
||||
return html`
|
||||
<onboarding-integrations
|
||||
@@ -133,9 +184,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
import("./onboarding-core-config");
|
||||
registerServiceWorker(this, false);
|
||||
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
|
||||
this.addEventListener("onboarding-progress", (ev) =>
|
||||
this._handleProgress(ev)
|
||||
);
|
||||
if (window.innerWidth > 450) {
|
||||
import("./particles");
|
||||
}
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -170,10 +225,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
|
||||
}
|
||||
|
||||
private _restoringBackup() {
|
||||
this._restoring = true;
|
||||
}
|
||||
|
||||
private async _fetchInstallationType(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchInstallationType();
|
||||
@@ -222,8 +273,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
});
|
||||
history.replaceState(null, "", location.pathname);
|
||||
await this._connectHass(auth);
|
||||
const currentStep = steps.findIndex((stp) => !stp.done);
|
||||
const singelStepProgress = 1 / steps.length;
|
||||
this._progress = currentStep * singelStepProgress + singelStepProgress;
|
||||
} else {
|
||||
// User creating screen needs to know the installation type.
|
||||
this._init = true;
|
||||
// Init screen needs to know the installation type.
|
||||
this._fetchInstallationType();
|
||||
}
|
||||
|
||||
@@ -233,15 +288,35 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleProgress(ev: HASSDomEvent<OnboardingProgressEvent>) {
|
||||
const stepSize = 1 / this._steps!.length;
|
||||
if (ev.detail.increase) {
|
||||
this._progress += ev.detail.increase * stepSize;
|
||||
}
|
||||
if (ev.detail.decrease) {
|
||||
this._progress -= ev.detail.decrease * stepSize;
|
||||
}
|
||||
if (ev.detail.progress) {
|
||||
this._progress = ev.detail.progress;
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
|
||||
const stepResult = ev.detail;
|
||||
this._steps = this._steps!.map((step) =>
|
||||
step.step === stepResult.type ? { ...step, done: true } : step
|
||||
);
|
||||
|
||||
if (stepResult.type === "user") {
|
||||
if (stepResult.type === "init") {
|
||||
this._init = false;
|
||||
this._restoring = stepResult.result.restore;
|
||||
if (!this._restoring) {
|
||||
this._progress = 0.25;
|
||||
}
|
||||
} else if (stepResult.type === "user") {
|
||||
const result = stepResult.result as OnboardingResponses["user"];
|
||||
this._loading = true;
|
||||
this._progress = 0.5;
|
||||
enableWrite();
|
||||
try {
|
||||
const auth = await getAuth({
|
||||
@@ -258,6 +333,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
this._loading = false;
|
||||
}
|
||||
} else if (stepResult.type === "core_config") {
|
||||
this._progress = 0.75;
|
||||
// We do nothing
|
||||
} else if (stepResult.type === "analytics") {
|
||||
this._progress = 1;
|
||||
// We do nothing
|
||||
} else if (stepResult.type === "integration") {
|
||||
this._loading = true;
|
||||
@@ -331,6 +410,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
subscribeOne(conn, subscribeUser),
|
||||
]);
|
||||
this.initializeHass(auth, conn);
|
||||
if (this.language && this.language !== this.hass!.language) {
|
||||
this._updateHass({
|
||||
locale: { ...this.hass!.locale, language: this.language },
|
||||
language: this.language,
|
||||
selectedLanguage: this.language,
|
||||
});
|
||||
storeState(this.hass!);
|
||||
}
|
||||
// Load config strings for integrations
|
||||
(this as any)._loadFragmentTranslations(this.hass!.language, "config");
|
||||
// Make sure hass is initialized + the config/user callbacks have called.
|
||||
@@ -338,6 +425,60 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
const language = ev.detail.value;
|
||||
this.language = language;
|
||||
if (this.hass) {
|
||||
this._updateHass({
|
||||
locale: { ...this.hass!.locale, language },
|
||||
language,
|
||||
selectedLanguage: language,
|
||||
});
|
||||
storeState(this.hass!);
|
||||
} else {
|
||||
try {
|
||||
localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
} catch (err: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
mwc-linear-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-language-picker {
|
||||
display: block;
|
||||
width: 200px;
|
||||
margin-top: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
--ha-select-height: 40px;
|
||||
--mdc-select-fill-color: none;
|
||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-idle-line-color: transparent;
|
||||
--mdc-select-hover-line-color: transparent;
|
||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -9,8 +9,6 @@ class IntegrationBadge extends LitElement {
|
||||
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public badgeIcon?: string;
|
||||
|
||||
@property({ type: Boolean }) public darkOptimizedIcon?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clickable = false;
|
||||
@@ -27,12 +25,6 @@ class IntegrationBadge extends LitElement {
|
||||
})}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
${this.badgeIcon
|
||||
? html`<ha-svg-icon
|
||||
class="badge"
|
||||
.path=${this.badgeIcon}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${this.title}</div>
|
||||
`;
|
||||
@@ -47,10 +39,6 @@ class IntegrationBadge extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
:host([clickable]) {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -66,18 +54,6 @@ class IntegrationBadge extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
color: white;
|
||||
bottom: -7px;
|
||||
right: -10px;
|
||||
background-color: var(--label-badge-green);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
--mdc-icon-size: 18px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.title {
|
||||
min-height: 2.3em;
|
||||
word-break: break-word;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -8,6 +9,7 @@ import { Analytics, setAnalyticsPreferences } from "../data/analytics";
|
||||
import { onboardAnalyticsStep } from "../data/onboarding";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
@customElement("onboarding-analytics")
|
||||
class OnboardingAnalytics extends LitElement {
|
||||
@@ -23,7 +25,18 @@ class OnboardingAnalytics extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h1>${this.localize("ui.panel.page-onboarding.analytics.header")}</h1>
|
||||
<p>${this.localize("ui.panel.page-onboarding.analytics.intro")}</p>
|
||||
<p>
|
||||
<a
|
||||
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</p>
|
||||
<ha-analytics
|
||||
translation_key_panel="page-onboarding"
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
@@ -33,16 +46,13 @@ class OnboardingAnalytics extends LitElement {
|
||||
</ha-analytics>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._save} .disabled=${!this._analyticsDetails}>
|
||||
<mwc-button
|
||||
unelevated
|
||||
@click=${this._save}
|
||||
.disabled=${!this._analyticsDetails}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.analytics.finish")}
|
||||
</mwc-button>
|
||||
<a
|
||||
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -81,27 +91,19 @@ class OnboardingAnalytics extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
// footer is direction reverse to tab to "NEXT" first
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,22 +13,12 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-country-picker";
|
||||
import "../components/ha-currency-picker";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-language-picker";
|
||||
import "../components/ha-radio";
|
||||
import type { HaRadio } from "../components/ha-radio";
|
||||
import "../components/ha-textfield";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/ha-timezone-picker";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../data/core";
|
||||
import { countryCurrency } from "../data/currency";
|
||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import "./onboarding-location";
|
||||
import "./onboarding-name";
|
||||
|
||||
@customElement("onboarding-core-config")
|
||||
class OnboardingCoreConfig extends LitElement {
|
||||
@@ -38,32 +28,26 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _name?: ConfigUpdateValues["location_name"];
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _elevation?: string;
|
||||
private _elevation = "0";
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
private _timeZone: ConfigUpdateValues["time_zone"] =
|
||||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
|
||||
@state() private _currency?: ConfigUpdateValues["currency"];
|
||||
|
||||
@state() private _timeZone?: ConfigUpdateValues["time_zone"];
|
||||
|
||||
@state() private _language: ConfigUpdateValues["language"];
|
||||
private _language: ConfigUpdateValues["language"] = getLocalLanguage();
|
||||
|
||||
@state() private _country?: ConfigUpdateValues["country"];
|
||||
|
||||
private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
private _currency?: ConfigUpdateValues["currency"];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _skipCore = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._name) {
|
||||
return html`<onboarding-name
|
||||
.hass=${this.hass}
|
||||
.onboardingLocalize=${this.onboardingLocalize}
|
||||
@value-changed=${this._nameChanged}
|
||||
></onboarding-name>`;
|
||||
}
|
||||
if (!this._location) {
|
||||
return html`<onboarding-location
|
||||
.hass=${this.hass}
|
||||
@@ -71,156 +55,34 @@ class OnboardingCoreConfig extends LitElement {
|
||||
@value-changed=${this._locationChanged}
|
||||
></onboarding-location>`;
|
||||
}
|
||||
if (this._skipCore) {
|
||||
return html`<div class="row center">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>`;
|
||||
}
|
||||
return html`
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_core_config"
|
||||
)}
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.country_intro"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<ha-country-picker
|
||||
class="flex"
|
||||
.language=${this.hass.locale.language}
|
||||
.label=${
|
||||
this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
) || "Country"
|
||||
}
|
||||
name="country"
|
||||
required
|
||||
.disabled=${this._working}
|
||||
.value=${this._countryValue}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-country-picker>
|
||||
<ha-language-picker
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
nativeName
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
)}
|
||||
name="language"
|
||||
required
|
||||
.value=${this._languageValue}
|
||||
.disabled=${this._working}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-language-picker>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<ha-timezone-picker
|
||||
class="flex"
|
||||
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
.disabled=${this._working}
|
||||
.value=${this._timeZoneValue}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-timezone-picker>
|
||||
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${this._working}
|
||||
.value=${this._elevationValue}
|
||||
.suffix=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystemValue === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.us_customary_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="us_customary"
|
||||
.checked=${this._unitSystemValue === "us_customary"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._working}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}<br />
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<ha-currency-picker
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
.disabled=${this._working}
|
||||
.value=${this._currencyValue}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-currency-picker
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ha-country-picker
|
||||
class="flex"
|
||||
.language=${this.hass.locale.language}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
) || "Country"}
|
||||
required
|
||||
.disabled=${this._working}
|
||||
.value=${this._countryValue}
|
||||
@value-changed=${this._handleCountryChanged}
|
||||
>
|
||||
</ha-country-picker>
|
||||
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._save} .disabled=${this._working}>
|
||||
@@ -232,20 +94,6 @@ class OnboardingCoreConfig extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (!changedProps.has("_country") || !this._country) {
|
||||
return;
|
||||
}
|
||||
if (!this._currency) {
|
||||
this._currency = countryCurrency[this._country];
|
||||
}
|
||||
if (!this._unitSystem) {
|
||||
this._unitSystem = ["US", "MM", "LR"].includes(this._country)
|
||||
? "us_customary"
|
||||
: "metric";
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("keyup", (ev) => {
|
||||
@@ -255,84 +103,69 @@ class OnboardingCoreConfig extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined ? this._elevation : 0;
|
||||
}
|
||||
|
||||
private get _timeZoneValue() {
|
||||
return this._timeZone || "";
|
||||
}
|
||||
|
||||
private get _languageValue() {
|
||||
return this._language || "";
|
||||
}
|
||||
|
||||
private get _countryValue() {
|
||||
return this._country || "";
|
||||
}
|
||||
|
||||
private get _unitSystemValue() {
|
||||
return this._unitSystem !== undefined ? this._unitSystem : "metric";
|
||||
}
|
||||
|
||||
private get _currencyValue() {
|
||||
return this._currency !== undefined ? this._currency : "";
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
this[`_${target.getAttribute("name")}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleChange(ev: Event) {
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private _nameChanged(ev: CustomEvent) {
|
||||
this._name = ev.detail.value;
|
||||
private _handleCountryChanged(ev: ValueChangedEvent<string>) {
|
||||
this._country = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _locationChanged(ev) {
|
||||
this._location = ev.detail.value.location;
|
||||
this._country = ev.detail.value.country;
|
||||
this._elevation = ev.detail.value.elevation;
|
||||
this._currency = ev.detail.value.currency;
|
||||
this._language = ev.detail.value.language || getLocalLanguage();
|
||||
this._timeZone =
|
||||
ev.detail.value.timezone ||
|
||||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
this._unitSystem = ev.detail.value.unit_system;
|
||||
if (ev.detail.value.country) {
|
||||
this._country = ev.detail.value.country;
|
||||
}
|
||||
if (ev.detail.value.elevation) {
|
||||
this._elevation = ev.detail.value.elevation;
|
||||
}
|
||||
if (ev.detail.value.currency) {
|
||||
this._currency = ev.detail.value.currency;
|
||||
}
|
||||
if (ev.detail.value.language) {
|
||||
this._language = ev.detail.value.language;
|
||||
}
|
||||
if (ev.detail.value.timezone) {
|
||||
this._timeZone = ev.detail.value.timezone;
|
||||
}
|
||||
if (ev.detail.value.unit_system) {
|
||||
this._unitSystem = ev.detail.value.unit_system;
|
||||
}
|
||||
if (this._country) {
|
||||
this._skipCore = true;
|
||||
this._save(ev);
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "onboarding-progress", { increase: 0.5 });
|
||||
await this.updateComplete;
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
() => this.renderRoot.querySelector("ha-country-picker")!.focus(),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as
|
||||
| "metric"
|
||||
| "us_customary";
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
if (!this._location) {
|
||||
if (!this._location || !this._country) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._name,
|
||||
location_name: this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name_default"
|
||||
),
|
||||
latitude: this._location[0],
|
||||
longitude: this._location[1],
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue || "UTC",
|
||||
currency: this._currencyValue || "EUR",
|
||||
country: this._countryValue,
|
||||
language: this._languageValue,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system:
|
||||
this._unitSystem || ["US", "MM", "LR"].includes(this._country)
|
||||
? "us_customary"
|
||||
: "metric",
|
||||
time_zone: this._timeZone || "UTC",
|
||||
currency: this._currency || countryCurrency[this._country] || "EUR",
|
||||
country: this._country,
|
||||
language: this._language,
|
||||
});
|
||||
const result = await onboardCoreConfigStep(this.hass);
|
||||
fireEvent(this, "onboarding-step", {
|
||||
@@ -340,6 +173,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
result,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._skipCore = false;
|
||||
this._working = false;
|
||||
this._error = err.message;
|
||||
}
|
||||
@@ -380,6 +214,10 @@ class OnboardingCoreConfig extends LitElement {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
@@ -16,6 +15,8 @@ import type { HaForm } from "../components/ha-form/ha-form";
|
||||
import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types";
|
||||
import { onboardUserStep } from "../data/onboarding";
|
||||
import { ValueChangedEvent } from "../types";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
const CREATE_USER_SCHEMA: HaFormSchema[] = [
|
||||
{
|
||||
@@ -58,7 +59,7 @@ class OnboardingCreateUser extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
|
||||
<h1>${this.localize("ui.panel.page-onboarding.user.header")}</h1>
|
||||
<p>${this.localize("ui.panel.page-onboarding.user.intro")}</p>
|
||||
|
||||
${this._errorMsg
|
||||
@@ -67,25 +68,27 @@ class OnboardingCreateUser extends LitElement {
|
||||
|
||||
<ha-form
|
||||
.computeLabel=${this._computeLabel(this.localize)}
|
||||
.computeHelper=${this._computeHelper(this.localize)}
|
||||
.data=${this._newUser}
|
||||
.disabled=${this._loading}
|
||||
.error=${this._formError}
|
||||
.schema=${CREATE_USER_SCHEMA}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-form>
|
||||
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._submitForm}
|
||||
.disabled=${this._loading ||
|
||||
!this._newUser.name ||
|
||||
!this._newUser.username ||
|
||||
!this._newUser.password ||
|
||||
!this._newUser.password_confirm ||
|
||||
this._newUser.password !== this._newUser.password_confirm}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.user.create_account")}
|
||||
</mwc-button>
|
||||
<div class="footer">
|
||||
<mwc-button
|
||||
unelevated
|
||||
@click=${this._submitForm}
|
||||
.disabled=${this._loading ||
|
||||
!this._newUser.name ||
|
||||
!this._newUser.username ||
|
||||
!this._newUser.password ||
|
||||
!this._newUser.password_confirm ||
|
||||
this._newUser.password !== this._newUser.password_confirm}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.user.create_account")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -111,20 +114,48 @@ class OnboardingCreateUser extends LitElement {
|
||||
localize(`ui.panel.page-onboarding.user.data.${schema.name}`);
|
||||
}
|
||||
|
||||
private _computeHelper(localize) {
|
||||
return (schema: HaFormSchema) =>
|
||||
localize(`ui.panel.page-onboarding.user.helper.${schema.name}`);
|
||||
}
|
||||
|
||||
private _handleValueChanged(
|
||||
ev: ValueChangedEvent<HaFormDataContainer>
|
||||
): void {
|
||||
const nameChanged = ev.detail.value.name !== this._newUser.name;
|
||||
const passwordChanged =
|
||||
ev.detail.value.password !== this._newUser.password ||
|
||||
ev.detail.value.password_confirm !== this._newUser.password_confirm;
|
||||
this._newUser = ev.detail.value;
|
||||
if (nameChanged) {
|
||||
this._maybePopulateUsername();
|
||||
}
|
||||
if (passwordChanged) {
|
||||
if (this._formError.password_confirm) {
|
||||
this._checkPasswordMatch();
|
||||
} else {
|
||||
this._debouncedCheckPasswordMatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _debouncedCheckPasswordMatch = debounce(
|
||||
() => this._checkPasswordMatch(),
|
||||
500
|
||||
);
|
||||
|
||||
private _checkPasswordMatch(): void {
|
||||
const old = this._formError.password_confirm;
|
||||
this._formError.password_confirm =
|
||||
this._newUser.password_confirm &&
|
||||
this._newUser.password !== this._newUser.password_confirm
|
||||
? this.localize(
|
||||
"ui.panel.page-onboarding.user.error.password_not_match"
|
||||
)
|
||||
: "";
|
||||
if (old !== this._formError.password_confirm) {
|
||||
this.requestUpdate("_formError");
|
||||
}
|
||||
}
|
||||
|
||||
private _maybePopulateUsername(): void {
|
||||
@@ -167,14 +198,7 @@ class OnboardingCreateUser extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
mwc-button {
|
||||
margin: 32px 0 0;
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
return onBoardingStyles;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCheck, mdiDotsHorizontal } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
@@ -16,23 +14,14 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
|
||||
import {
|
||||
getConfigFlowInProgressCollection,
|
||||
localizeConfigFlowTitle,
|
||||
subscribeConfigFlowInProgress,
|
||||
} from "../data/config_flow";
|
||||
import { subscribeConfigFlowInProgress } from "../data/config_flow";
|
||||
import { DataEntryFlowProgress } from "../data/data_entry_flow";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { scanUSBDevices } from "../data/usb";
|
||||
import {
|
||||
loadConfigFlowDialog,
|
||||
showConfigFlowDialog,
|
||||
} from "../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showAddIntegrationDialog } from "../panels/config/integrations/show-add-integration-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./action-badge";
|
||||
import "./integration-badge";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
const HIDDEN_DOMAINS = new Set([
|
||||
"hassio",
|
||||
@@ -63,7 +52,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
integrations.add(flow.handler);
|
||||
}
|
||||
}
|
||||
this.hass.loadBackendTranslation("config", Array.from(integrations));
|
||||
this.hass.loadBackendTranslation("title", Array.from(integrations));
|
||||
}),
|
||||
subscribeConfigEntries(
|
||||
this.hass,
|
||||
@@ -109,65 +98,68 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
// Render discovered and existing entries together sorted by localized title.
|
||||
const entries: Array<[string, TemplateResult]> = this._entries.map(
|
||||
(entry) => {
|
||||
const title =
|
||||
entry.title ||
|
||||
domainToName(this.hass.localize, entry.domain) ||
|
||||
entry.domain;
|
||||
return [
|
||||
title,
|
||||
html`
|
||||
<integration-badge
|
||||
.domain=${entry.domain}
|
||||
.title=${title}
|
||||
.badgeIcon=${mdiCheck}
|
||||
.darkOptimizedIcon=${this.hass.themes?.darkMode}
|
||||
></integration-badge>
|
||||
`,
|
||||
];
|
||||
}
|
||||
const entries: Array<[string, string]> = this._entries.map((entry) => [
|
||||
entry.domain,
|
||||
domainToName(this.hass.localize, entry.domain),
|
||||
]);
|
||||
const discovered: Array<[string, string]> = this._discovered.map((flow) => [
|
||||
flow.handler,
|
||||
domainToName(this.hass.localize, flow.handler),
|
||||
]);
|
||||
let domains = [...entries, ...discovered].sort((a, b) =>
|
||||
stringCompare(a[0], b[0], this.hass.locale.language)
|
||||
);
|
||||
const discovered: Array<[string, TemplateResult]> = this._discovered.map(
|
||||
(flow) => {
|
||||
const title = localizeConfigFlowTitle(this.hass.localize, flow);
|
||||
return [
|
||||
title,
|
||||
html`
|
||||
<button .flowId=${flow.flow_id} @click=${this._continueFlow}>
|
||||
<integration-badge
|
||||
clickable
|
||||
.domain=${flow.handler}
|
||||
.title=${title}
|
||||
.darkOptimizedIcon=${this.hass.themes?.darkMode}
|
||||
></integration-badge>
|
||||
</button>
|
||||
`,
|
||||
];
|
||||
|
||||
const foundDevices = domains.length;
|
||||
|
||||
if (domains.length > 12) {
|
||||
const uniqueDomains: Set<string> = new Set();
|
||||
domains.forEach(([domain]) => {
|
||||
uniqueDomains.add(domain);
|
||||
});
|
||||
if (uniqueDomains.size < domains.length) {
|
||||
domains = domains.filter(([domain]) => {
|
||||
if (uniqueDomains.has(domain)) {
|
||||
uniqueDomains.delete(domain);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
);
|
||||
const content = [...entries, ...discovered]
|
||||
.sort((a, b) => stringCompare(a[0], b[0], this.hass.locale.language))
|
||||
.map((item) => item[1]);
|
||||
if (domains.length > 12) {
|
||||
domains = domains.slice(0, 11);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<h1>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.header"
|
||||
)}
|
||||
</h1>
|
||||
<p>
|
||||
${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")}
|
||||
</p>
|
||||
<div class="badges">
|
||||
${content}
|
||||
<button @click=${this._createFlow}>
|
||||
<action-badge
|
||||
clickable
|
||||
title=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.more_integrations"
|
||||
)}
|
||||
.icon=${mdiDotsHorizontal}
|
||||
></action-badge>
|
||||
</button>
|
||||
${domains.map(
|
||||
([domain, title]) =>
|
||||
html`<integration-badge
|
||||
.domain=${domain}
|
||||
.title=${title}
|
||||
.darkOptimizedIcon=${this.hass.themes?.darkMode}
|
||||
></integration-badge>`
|
||||
)}
|
||||
${foundDevices > domains.length
|
||||
? html`<div class="more">
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.more_integrations",
|
||||
{ count: foundDevices - domains.length }
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._finish}>
|
||||
<mwc-button unelevated @click=${this._finish}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.finish"
|
||||
)}
|
||||
@@ -178,22 +170,8 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("title", undefined, true);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
this._scanUSBDevices();
|
||||
loadConfigFlowDialog();
|
||||
}
|
||||
|
||||
private _createFlow() {
|
||||
showAddIntegrationDialog(this);
|
||||
}
|
||||
|
||||
private _continueFlow(ev) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: ev.currentTarget.flowId,
|
||||
dialogClosedCallback: () => {
|
||||
getConfigFlowInProgressCollection(this.hass!.connection).refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _scanUSBDevices() {
|
||||
@@ -210,34 +188,23 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.badges {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.badges > * {
|
||||
width: 96px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: 0;
|
||||
font: inherit;
|
||||
}
|
||||
.footer {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
.badges {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(106px, 1fr));
|
||||
row-gap: 24px;
|
||||
}
|
||||
.more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { customElement } from "lit/decorators";
|
||||
@customElement("onboarding-loading")
|
||||
class OnboardingLoading extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html` <div class="loader"></div> `;
|
||||
return html`<div class="loader"></div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCrosshairsGps, mdiMapMarker, mdiMapSearchOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiCrosshairsGps,
|
||||
mdiMagnify,
|
||||
mdiMapMarker,
|
||||
mdiMapSearchOutline,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -30,6 +35,7 @@ import {
|
||||
reverseGeocode,
|
||||
searchPlaces,
|
||||
} from "../data/openstreetmap";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
@@ -43,7 +49,7 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
@state() private _location: [number, number] = AMSTERDAM;
|
||||
|
||||
@state() private _places?: OpenStreetMapPlace[] | null;
|
||||
|
||||
@@ -87,6 +93,11 @@ class OnboardingLocation extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<h1>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_header"
|
||||
)}
|
||||
</h1>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
@@ -97,78 +108,85 @@ class OnboardingLocation extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-textfield
|
||||
label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.address_label"
|
||||
)}
|
||||
.disabled=${this._working}
|
||||
iconTrailing
|
||||
@keyup=${this._addressSearch}
|
||||
>
|
||||
${this._working
|
||||
<div class="location-search">
|
||||
<ha-textfield
|
||||
label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.address_label"
|
||||
)}
|
||||
.disabled=${this._working}
|
||||
icon
|
||||
iconTrailing
|
||||
@keyup=${this._addressSearch}
|
||||
>
|
||||
<ha-svg-icon slot="leadingIcon" .path=${mdiMagnify}></ha-svg-icon>
|
||||
${this._working
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
slot="trailingIcon"
|
||||
active
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleButtonClick}
|
||||
slot="trailingIcon"
|
||||
.disabled=${this._working}
|
||||
.label=${this.onboardingLocalize(
|
||||
this._search
|
||||
? "ui.common.search"
|
||||
: "ui.panel.page-onboarding.core-config.button_detect"
|
||||
)}
|
||||
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
${this._places !== undefined
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
slot="trailingIcon"
|
||||
active
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
<mwc-list activatable>
|
||||
${this._places?.length
|
||||
? this._places.map((place) => {
|
||||
const primary = [
|
||||
place.name || place.address[place.category],
|
||||
place.address.house_number,
|
||||
place.address.road || place.address.waterway,
|
||||
place.address.village || place.address.town,
|
||||
place.address.suburb || place.address.subdivision,
|
||||
place.address.city || place.address.municipality,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const secondary = [
|
||||
place.address.county ||
|
||||
place.address.state_district ||
|
||||
place.address.region,
|
||||
place.address.state,
|
||||
place.address.country,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
return html`<ha-list-item
|
||||
@click=${this._itemClicked}
|
||||
.placeId=${place.place_id}
|
||||
.selected=${this._highlightedMarker === place.place_id}
|
||||
.activated=${this._highlightedMarker === place.place_id}
|
||||
.twoline=${primary && secondary}
|
||||
>
|
||||
${primary || secondary}
|
||||
<span slot="secondary"
|
||||
>${primary ? secondary : ""}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
})
|
||||
: html`<ha-list-item noninteractive
|
||||
>${this._places === null
|
||||
? ""
|
||||
: "No results"}</ha-list-item
|
||||
>`}
|
||||
</mwc-list>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleButtonClick}
|
||||
slot="trailingIcon"
|
||||
.disabled=${this._working}
|
||||
.label=${this.onboardingLocalize(
|
||||
this._search
|
||||
? "ui.common.search"
|
||||
: "ui.panel.page-onboarding.core-config.button_detect"
|
||||
)}
|
||||
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
${this._places !== undefined
|
||||
? html`
|
||||
<mwc-list activatable>
|
||||
${this._places?.length
|
||||
? this._places.map((place) => {
|
||||
const primary = [
|
||||
place.name || place.address[place.category],
|
||||
place.address.house_number,
|
||||
place.address.road || place.address.waterway,
|
||||
place.address.village || place.address.town,
|
||||
place.address.suburb || place.address.subdivision,
|
||||
place.address.city || place.address.municipality,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const secondary = [
|
||||
place.address.county ||
|
||||
place.address.state_district ||
|
||||
place.address.region,
|
||||
place.address.state,
|
||||
place.address.country,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
return html`<ha-list-item
|
||||
@click=${this._itemClicked}
|
||||
.placeId=${place.place_id}
|
||||
.selected=${this._highlightedMarker === place.place_id}
|
||||
.activated=${this._highlightedMarker === place.place_id}
|
||||
.twoline=${primary && secondary}
|
||||
>
|
||||
${primary || secondary}
|
||||
<span slot="secondary">${primary ? secondary : ""}</span>
|
||||
</ha-list-item>`;
|
||||
})
|
||||
: html`<ha-list-item noninteractive
|
||||
>${this._places === null ? "" : "No results"}</ha-list-item
|
||||
>`}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
<p class="attribution">${addressAttribution}</p>
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
@@ -184,11 +202,10 @@ class OnboardingLocation extends LitElement {
|
||||
@marker-clicked=${this._markerClicked}
|
||||
></ha-locations-editor>
|
||||
|
||||
<p class="attribution">${addressAttribution}</p>
|
||||
|
||||
<div class="footer">
|
||||
<mwc-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._location || this._working}
|
||||
>
|
||||
<mwc-button @click=${this._save} unelevated .disabled=${this._working}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.finish"
|
||||
)}
|
||||
@@ -301,7 +318,6 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
private async _searchAddress(address: string) {
|
||||
this._working = true;
|
||||
this._location = undefined;
|
||||
this._highlightedMarker = undefined;
|
||||
this._error = undefined;
|
||||
this._places = null;
|
||||
@@ -320,7 +336,7 @@ class OnboardingLocation extends LitElement {
|
||||
);
|
||||
try {
|
||||
this._places = await searchPlaces(address, this.hass, true, 3);
|
||||
if (this._places?.length === 1) {
|
||||
if (this._places?.length) {
|
||||
this._highlightedMarker = this._places[0].place_id;
|
||||
this._location = [
|
||||
Number(this._places[0].lat),
|
||||
@@ -464,74 +480,76 @@ class OnboardingLocation extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield > ha-circular-progress {
|
||||
position: relative;
|
||||
left: 12px;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 300px;
|
||||
margin-top: 8px;
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
overflow: hidden;
|
||||
}
|
||||
mwc-list {
|
||||
width: 100%;
|
||||
border: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
border-top-width: 0;
|
||||
border-bottom-left-radius: var(--mdc-shape-small, 4px);
|
||||
border-bottom-right-radius: var(--mdc-shape-small, 4px);
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-list-item {
|
||||
height: 72px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
.attribution {
|
||||
/* textfield helper style */
|
||||
margin: 0;
|
||||
padding: 4px 16px 12px 16px;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-family: var(
|
||||
--mdc-typography-caption-font-family,
|
||||
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||
);
|
||||
font-size: var(--mdc-typography-caption-font-size, 0.75rem);
|
||||
font-weight: var(--mdc-typography-caption-font-weight, 400);
|
||||
letter-spacing: var(
|
||||
--mdc-typography-caption-letter-spacing,
|
||||
0.0333333333em
|
||||
);
|
||||
text-decoration: var(--mdc-typography-caption-text-decoration, inherit);
|
||||
text-transform: var(--mdc-typography-caption-text-transform, inherit);
|
||||
}
|
||||
.attribution a {
|
||||
color: inherit;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
.location-search {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield > ha-circular-progress {
|
||||
position: relative;
|
||||
left: 12px;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 300px;
|
||||
margin-top: 8px;
|
||||
border-radius: var(--mdc-shape-large, 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
mwc-list {
|
||||
width: 100%;
|
||||
border: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
border-top-width: 0;
|
||||
border-bottom-left-radius: var(--mdc-shape-small, 4px);
|
||||
border-bottom-right-radius: var(--mdc-shape-small, 4px);
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-list-item {
|
||||
height: 72px;
|
||||
}
|
||||
.attribution {
|
||||
/* textfield helper style */
|
||||
margin: 0;
|
||||
padding: 4px 16px 12px 16px;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-family: var(
|
||||
--mdc-typography-caption-font-family,
|
||||
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||
);
|
||||
font-size: var(--mdc-typography-caption-font-size, 0.75rem);
|
||||
font-weight: var(--mdc-typography-caption-font-weight, 400);
|
||||
letter-spacing: var(
|
||||
--mdc-typography-caption-letter-spacing,
|
||||
0.0333333333em
|
||||
);
|
||||
text-decoration: var(
|
||||
--mdc-typography-caption-text-decoration,
|
||||
inherit
|
||||
);
|
||||
text-transform: var(--mdc-typography-caption-text-transform, inherit);
|
||||
}
|
||||
.attribution a {
|
||||
color: inherit;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,111 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-textfield";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues } from "../data/core";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("onboarding-name")
|
||||
class OnboardingName extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public onboardingLocalize!: LocalizeFunc;
|
||||
|
||||
private _name?: ConfigUpdateValues["location_name"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro",
|
||||
{ name: this.hass.user!.name }
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-textfield
|
||||
.label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name"
|
||||
)}
|
||||
.value=${this._nameValue}
|
||||
@change=${this._nameChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_core"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._save} .disabled=${!this._nameValue}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.finish"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
this.addEventListener("keyup", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this._save(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private get _nameValue() {
|
||||
return this._name !== undefined
|
||||
? this._name
|
||||
: this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name_default"
|
||||
);
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
this._name = ev.target.value;
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
ev.preventDefault();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._nameValue,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-name": OnboardingName;
|
||||
}
|
||||
}
|
@@ -1,44 +1,34 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import "../components/ha-card";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./onboarding-loading";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
restoring: undefined;
|
||||
}
|
||||
}
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
@customElement("onboarding-restore-backup")
|
||||
class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
class OnboardingRestoreBackup extends LitElement {
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@property() public language!: string;
|
||||
|
||||
@property({ type: Boolean }) public restoring = false;
|
||||
@state() public _restoring = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.restoring
|
||||
? html`<ha-card
|
||||
.header=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.in_progress"
|
||||
)}
|
||||
>
|
||||
<onboarding-loading></onboarding-loading>
|
||||
</ha-card>`
|
||||
return this._restoring
|
||||
? html`<h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</h1>
|
||||
<onboarding-loading></onboarding-loading>`
|
||||
: html`
|
||||
<button class="link" @click=${this._uploadBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.restore.description")}
|
||||
</button>
|
||||
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||
<ha-button unelevated @click=${this._uploadBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -51,12 +41,11 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
setInterval(() => this._checkRestoreStatus(), 1000);
|
||||
}
|
||||
|
||||
private async _checkRestoreStatus(): Promise<void> {
|
||||
if (this.restoring) {
|
||||
if (this._restoring) {
|
||||
try {
|
||||
await fetchInstallationType();
|
||||
} catch (err: any) {
|
||||
@@ -72,32 +61,20 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
slug,
|
||||
onboarding: true,
|
||||
localize: this.localize,
|
||||
onRestoring: () => {
|
||||
this._restoring = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
onBoardingStyles,
|
||||
css`
|
||||
.logentry {
|
||||
text-align: center;
|
||||
}
|
||||
ha-card {
|
||||
padding: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-ansi-to-html {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
padding: 0 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media all and (min-width: 600px) {
|
||||
ha-card {
|
||||
width: 600px;
|
||||
margin-left: -100px;
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
103
src/onboarding/onboarding-welcome-link.ts
Normal file
103
src/onboarding/onboarding-welcome-link.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import "@material/mwc-ripple";
|
||||
import type { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import "../components/ha-card";
|
||||
|
||||
@customElement("onboarding-welcome-link")
|
||||
class OnboardingWelcomeLink extends LitElement {
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public iconPath!: string;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>
|
||||
${this.label}
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--text-primary-color);
|
||||
background: var(--welcome-link-color, var(--primary-color));
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-welcome-link": OnboardingWelcomeLink;
|
||||
}
|
||||
}
|
84
src/onboarding/onboarding-welcome-links.ts
Normal file
84
src/onboarding/onboarding-welcome-links.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showAppDialog } from "./dialogs/show-app-dialog";
|
||||
import { showCommunityDialog } from "./dialogs/show-community-dialog";
|
||||
import "./onboarding-welcome-link";
|
||||
|
||||
@customElement("onboarding-welcome-links")
|
||||
class OnboardingWelcomeLinks extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/"
|
||||
>
|
||||
<onboarding-welcome-link
|
||||
.iconPath=${mdiFileDocument}
|
||||
.label=${this.localize("ui.panel.page-onboarding.welcome.vision")}
|
||||
>
|
||||
</onboarding-welcome-link>
|
||||
</a>
|
||||
<onboarding-welcome-link
|
||||
class="community"
|
||||
@click=${this._openCommunity}
|
||||
.iconPath=${mdiAccountGroup}
|
||||
.label=${this.localize("ui.panel.page-onboarding.welcome.community")}
|
||||
>
|
||||
</onboarding-welcome-link>
|
||||
<onboarding-welcome-link
|
||||
class="app"
|
||||
@click=${this._openApp}
|
||||
.iconPath=${mdiTabletCellphone}
|
||||
.label=${this.localize("ui.panel.page-onboarding.welcome.download_app")}
|
||||
>
|
||||
</onboarding-welcome-link>`;
|
||||
}
|
||||
|
||||
private _openCommunity(): void {
|
||||
showCommunityDialog(this, { localize: this.localize });
|
||||
}
|
||||
|
||||
private _openApp(): void {
|
||||
showAppDialog(this, { localize: this.localize });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
margin-top: 16px;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
:host {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.community {
|
||||
--welcome-link-color: #008142;
|
||||
}
|
||||
.app {
|
||||
--welcome-link-color: #6e41ab;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-welcome-links": OnboardingWelcomeLinks;
|
||||
}
|
||||
}
|
79
src/onboarding/onboarding-welcome.ts
Normal file
79
src/onboarding/onboarding-welcome.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "../components/ha-button";
|
||||
|
||||
@customElement("onboarding-welcome")
|
||||
class OnboardingWelcome extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public supervisor?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
|
||||
|
||||
<ha-button unelevated @click=${this._start} class="start">
|
||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||
</ha-button>
|
||||
|
||||
${this.supervisor
|
||||
? html`<ha-button @click=${this._restoreBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
fireEvent(this, "onboarding-step", {
|
||||
type: "init",
|
||||
result: { restore: false },
|
||||
});
|
||||
}
|
||||
|
||||
private _restoreBackup(): void {
|
||||
fireEvent(this, "onboarding-step", {
|
||||
type: "init",
|
||||
result: { restore: true },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.start {
|
||||
--button-height: 48px;
|
||||
--mdc-typography-button-font-size: 1rem;
|
||||
--mdc-button-horizontal-padding: 24px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-welcome": OnboardingWelcome;
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { tsParticles } from "tsparticles-engine";
|
||||
import { loadLinksPreset } from "tsparticles-preset-links";
|
||||
import { DEFAULT_PRIMARY_COLOR } from "../resources/ha-style";
|
||||
|
||||
loadLinksPreset(tsParticles).then(() => {
|
||||
tsParticles.load("particles", {
|
||||
@@ -22,16 +23,16 @@ loadLinksPreset(tsParticles).then(() => {
|
||||
},
|
||||
particles: {
|
||||
color: {
|
||||
value: "#fff",
|
||||
animation: {
|
||||
enable: true,
|
||||
speed: 50,
|
||||
sync: false,
|
||||
},
|
||||
value: DEFAULT_PRIMARY_COLOR,
|
||||
},
|
||||
animation: {
|
||||
enable: true,
|
||||
speed: 50,
|
||||
sync: false,
|
||||
},
|
||||
links: {
|
||||
color: {
|
||||
value: "random",
|
||||
value: DEFAULT_PRIMARY_COLOR,
|
||||
},
|
||||
distance: 100,
|
||||
enable: true,
|
||||
|
19
src/onboarding/styles.ts
Normal file
19
src/onboarding/styles.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const onBoardingStyles = css`
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
@@ -100,6 +100,14 @@ class DialogAreaDetail extends LitElement {
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
||||
<div class="label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.aliases_section"
|
||||
@@ -132,14 +140,6 @@ class DialogAreaDetail extends LitElement {
|
||||
"ui.panel.config.areas.editor.aliases_description"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
</div>
|
||||
</div>
|
||||
${entry
|
||||
@@ -229,7 +229,8 @@ class DialogAreaDetail extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-textfield {
|
||||
ha-textfield,
|
||||
ha-picture-upload {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -16,9 +15,7 @@ import {
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
fetchZwaveNodeStatus,
|
||||
nodeStatus,
|
||||
SecurityClass,
|
||||
subscribeZwaveNodeStatus,
|
||||
ZWaveJSNodeStatus,
|
||||
} from "../../../../../../data/zwave_js";
|
||||
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
|
||||
@@ -44,21 +41,6 @@ export class HaDeviceInfoZWaveJS extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||
return [
|
||||
subscribeZwaveNodeStatus(this.hass, this.device!.id, (message) => {
|
||||
if (!this._node) {
|
||||
return;
|
||||
}
|
||||
this._node = {
|
||||
...this._node,
|
||||
status: message.status,
|
||||
ready: message.ready,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected async _fetchNodeDetails() {
|
||||
if (!this.device) {
|
||||
return;
|
||||
@@ -112,16 +94,6 @@ export class HaDeviceInfoZWaveJS extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
${!this._node.is_controller_node
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.node_status"
|
||||
)}:
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_status.${
|
||||
nodeStatus[this._node.status]
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.node_ready"
|
||||
|
@@ -110,10 +110,10 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFileUpload}
|
||||
label=${this._firmwareFile?.name ??
|
||||
this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.upload_firmware"
|
||||
)}
|
||||
.value=${this._firmwareFile}
|
||||
@file-picked=${this._uploadFile}
|
||||
></ha-file-upload>
|
||||
${this._nodeStatus.is_controller_node
|
||||
|
@@ -126,6 +126,7 @@ class DialogPersonDetail extends LitElement {
|
||||
)}
|
||||
required
|
||||
></ha-textfield>
|
||||
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
@@ -422,7 +423,8 @@ class DialogPersonDetail extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
ha-picture-upload {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
--file-upload-image-border-radius: 50%;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
|
@@ -236,7 +236,7 @@ class DialogZoneDetail extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
--mdc-dialog-min-width: min(600px, 95vw);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
|
@@ -19,7 +19,7 @@ import {
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import type HaChartBase from "../../../../components/chart/ha-chart-base";
|
||||
import type { HaChartBase } from "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
@@ -130,7 +130,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: { bar: { borderWidth: 1.5, borderRadius: 4 } },
|
||||
elements: { bar: { borderWidth: 1, borderRadius: 4 } },
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: "nearest",
|
||||
@@ -292,6 +292,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
});
|
||||
|
||||
this._chartData = {
|
||||
labels: chartData.map((d) => d.y),
|
||||
datasets,
|
||||
};
|
||||
await this.updateComplete;
|
||||
|
@@ -44,6 +44,11 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyUsageGraphCardConfig } from "../types";
|
||||
|
||||
interface ColorSet {
|
||||
base: string;
|
||||
overrides?: Record<string, string>;
|
||||
}
|
||||
|
||||
@customElement("hui-energy-usage-graph-card")
|
||||
export class HuiEnergyUsageGraphCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
@@ -354,26 +359,68 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const colors = {
|
||||
to_grid: computedStyles
|
||||
.getPropertyValue("--energy-grid-return-color")
|
||||
.trim(),
|
||||
to_battery: computedStyles
|
||||
.getPropertyValue("--energy-battery-in-color")
|
||||
.trim(),
|
||||
from_grid: computedStyles
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim(),
|
||||
used_grid: computedStyles
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim(),
|
||||
used_solar: computedStyles
|
||||
.getPropertyValue("--energy-solar-color")
|
||||
.trim(),
|
||||
used_battery: computedStyles
|
||||
.getPropertyValue("--energy-battery-out-color")
|
||||
.trim(),
|
||||
|
||||
const colorPropertyMap = {
|
||||
to_grid: "--energy-grid-return-color",
|
||||
to_battery: "--energy-battery-in-color",
|
||||
from_grid: "--energy-grid-consumption-color",
|
||||
used_grid: "--energy-grid-consumption-color",
|
||||
used_solar: "--energy-solar-color",
|
||||
used_battery: "--energy-battery-out-color",
|
||||
};
|
||||
|
||||
const colors = {
|
||||
to_grid: {
|
||||
base: computedStyles.getPropertyValue(colorPropertyMap.to_grid).trim(),
|
||||
},
|
||||
to_battery: {
|
||||
base: computedStyles
|
||||
.getPropertyValue(colorPropertyMap.to_battery)
|
||||
.trim(),
|
||||
},
|
||||
from_grid: {
|
||||
base: computedStyles
|
||||
.getPropertyValue(colorPropertyMap.from_grid)
|
||||
.trim(),
|
||||
},
|
||||
used_grid: {
|
||||
base: computedStyles
|
||||
.getPropertyValue(colorPropertyMap.used_grid)
|
||||
.trim(),
|
||||
},
|
||||
used_solar: {
|
||||
base: computedStyles
|
||||
.getPropertyValue(colorPropertyMap.used_solar)
|
||||
.trim(),
|
||||
},
|
||||
used_battery: {
|
||||
base: computedStyles
|
||||
.getPropertyValue(colorPropertyMap.used_battery)
|
||||
.trim(),
|
||||
},
|
||||
};
|
||||
|
||||
Object.entries(colorPropertyMap).forEach(([key, colorProp]) => {
|
||||
if (
|
||||
key === "used_grid" ||
|
||||
key === "used_solar" ||
|
||||
key === "used_battery"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
colors[key].overrides = [];
|
||||
if (statIds[key]) {
|
||||
Object.values(statIds[key]).forEach((id, idx) => {
|
||||
const override = computedStyles
|
||||
.getPropertyValue(colorProp + "-" + idx)
|
||||
.trim();
|
||||
if (override.length > 0) {
|
||||
colors[key].overrides[id] = override;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const labels = {
|
||||
used_grid: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.combined_from_grid"
|
||||
@@ -443,12 +490,12 @@ export class HuiEnergyUsageGraphCard
|
||||
from_battery?: string[] | undefined;
|
||||
},
|
||||
colors: {
|
||||
to_grid: string;
|
||||
to_battery: string;
|
||||
from_grid: string;
|
||||
used_grid: string;
|
||||
used_solar: string;
|
||||
used_battery: string;
|
||||
to_grid: ColorSet;
|
||||
to_battery: ColorSet;
|
||||
from_grid: ColorSet;
|
||||
used_grid: ColorSet;
|
||||
used_solar: ColorSet;
|
||||
used_battery: ColorSet;
|
||||
},
|
||||
labels: {
|
||||
used_grid: string;
|
||||
@@ -598,15 +645,18 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
Object.entries(sources).forEach(([statId, source], idx) => {
|
||||
const modifiedColor =
|
||||
idx > 0
|
||||
? this.hass.themes.darkMode
|
||||
? labBrighten(rgb2lab(hex2rgb(colors[type])), idx)
|
||||
: labDarken(rgb2lab(hex2rgb(colors[type])), idx)
|
||||
: undefined;
|
||||
const borderColor = modifiedColor
|
||||
? rgb2hex(lab2rgb(modifiedColor))
|
||||
: colors[type];
|
||||
let borderColor = colors[type].overrides?.[statId];
|
||||
if (!borderColor) {
|
||||
const modifiedColor =
|
||||
idx > 0
|
||||
? this.hass.themes.darkMode
|
||||
? labBrighten(rgb2lab(hex2rgb(colors[type].base)), idx)
|
||||
: labDarken(rgb2lab(hex2rgb(colors[type].base)), idx)
|
||||
: undefined;
|
||||
borderColor = modifiedColor
|
||||
? rgb2hex(lab2rgb(modifiedColor))
|
||||
: colors[type].base;
|
||||
}
|
||||
|
||||
const points: ScatterDataPoint[] = [];
|
||||
// Process chart data.
|
||||
|
@@ -121,10 +121,7 @@ class HuiClimateHvacModeTileFeature
|
||||
.value=${this._currentHvacMode}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-label
|
||||
.ariaLabel=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"hvac_mode"
|
||||
)}
|
||||
.ariaLabel=${this.hass.localize("ui.card.climate.mode")}
|
||||
style=${styleMap({
|
||||
"--control-select-color": color,
|
||||
})}
|
||||
|
@@ -125,10 +125,7 @@ class HuiWaterHeaterOperationModeTileFeature
|
||||
.value=${this._currentOperationMode}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-label
|
||||
.ariaLabel=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"hvac_mode"
|
||||
)}
|
||||
.ariaLabel=${this.hass.localize("ui.card.water_heater.mode")}
|
||||
style=${styleMap({
|
||||
"--control-select-color": color,
|
||||
})}
|
||||
|
@@ -1,158 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-icon-button";
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import "../../styles/polymer-ha-style-dialog";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaDialogShowAudioMessage extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
text-align: var(--float-end);
|
||||
}
|
||||
</style>
|
||||
<ha-dialog
|
||||
open="[[_opened]]"
|
||||
on-closed="closeDialog"
|
||||
heading="[[localize('ui.panel.mailbox.playback_title')]]"
|
||||
>
|
||||
<div>
|
||||
<div class="icon">
|
||||
<template is="dom-if" if="[[_loading]]">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</template>
|
||||
<ha-icon-button id="delicon" on-click="openDeleteDialog">
|
||||
<ha-icon icon="hass:delete"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
<div id="transcribe"></div>
|
||||
<div>
|
||||
<template is="dom-if" if="[[_errorMsg]]">
|
||||
<div class="error">[[_errorMsg]]</div>
|
||||
</template>
|
||||
<audio id="mp3" preload="none" controls>
|
||||
<source id="mp3src" src="" type="audio/mpeg" />
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
_currentMessage: Object,
|
||||
|
||||
// Error message when can't talk to server etc
|
||||
_errorMsg: String,
|
||||
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_opened: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
showDialog({ hass, message }) {
|
||||
this.hass = hass;
|
||||
this._errorMsg = null;
|
||||
this._currentMessage = message;
|
||||
this._opened = true;
|
||||
this.$.transcribe.innerText = message.message;
|
||||
const platform = message.platform;
|
||||
const mp3 = this.$.mp3;
|
||||
if (platform.has_media) {
|
||||
mp3.style.display = "";
|
||||
this._showLoading(true);
|
||||
mp3.src = null;
|
||||
const url = `/api/mailbox/media/${platform.name}/${message.sha}`;
|
||||
this.hass
|
||||
.fetchWithAuth(url)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.blob();
|
||||
}
|
||||
return Promise.reject({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
})
|
||||
.then((blob) => {
|
||||
this._showLoading(false);
|
||||
mp3.src = window.URL.createObjectURL(blob);
|
||||
mp3.play();
|
||||
})
|
||||
.catch((err) => {
|
||||
this._showLoading(false);
|
||||
this._errorMsg = `Error loading audio: ${err.statusText}`;
|
||||
});
|
||||
} else {
|
||||
mp3.style.display = "none";
|
||||
this._showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
openDeleteDialog() {
|
||||
if (confirm(this.localize("ui.panel.mailbox.delete_prompt"))) {
|
||||
this.deleteSelected();
|
||||
}
|
||||
}
|
||||
|
||||
deleteSelected() {
|
||||
const msg = this._currentMessage;
|
||||
this.hass.callApi(
|
||||
"DELETE",
|
||||
`mailbox/delete/${msg.platform.name}/${msg.sha}`
|
||||
);
|
||||
this._dialogDone();
|
||||
}
|
||||
|
||||
_dialogDone() {
|
||||
this.$.mp3.pause();
|
||||
this.setProperties({
|
||||
_currentMessage: null,
|
||||
_errorMsg: null,
|
||||
_loading: false,
|
||||
_opened: false,
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this._dialogDone();
|
||||
}
|
||||
|
||||
_showLoading(displayed) {
|
||||
const delicon = this.$.delicon;
|
||||
if (displayed) {
|
||||
this._loading = true;
|
||||
delicon.style.display = "none";
|
||||
} else {
|
||||
const platform = this._currentMessage.platform;
|
||||
this._loading = false;
|
||||
delicon.style.display = platform.can_delete ? "" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("ha-dialog-show-audio-message", HaDialogShowAudioMessage);
|
153
src/panels/mailbox/ha-dialog-show-audio-message.ts
Normal file
153
src/panels/mailbox/ha-dialog-show-audio-message.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import "@material/mwc-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-icon-button";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
|
||||
@customElement("ha-dialog-show-audio-message")
|
||||
class HaDialogShowAudioMessage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _currentMessage?: any;
|
||||
|
||||
@state() private _errorMsg?: string;
|
||||
|
||||
@state() private _loading: boolean = false;
|
||||
|
||||
@state() private _opened: boolean = false;
|
||||
|
||||
@state() private _blobUrl?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this._closeDialog}
|
||||
heading=${this.hass.localize("ui.panel.mailbox.playback_title")}
|
||||
>
|
||||
${this._loading
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<div class="icon">
|
||||
<ha-icon-button id="delicon" @click=${this._openDeleteDialog}>
|
||||
<ha-icon icon="hass:delete"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
${
|
||||
this._currentMessage
|
||||
? html`<div id="transcribe">
|
||||
${this._currentMessage?.message}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._errorMsg
|
||||
? html`<div class="error">${this._errorMsg}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._blobUrl
|
||||
? html` <audio id="mp3" preload="none" controls autoplay>
|
||||
<source
|
||||
id="mp3src"
|
||||
src=${this._blobUrl}
|
||||
type="audio/mpeg"
|
||||
/>
|
||||
</audio>`
|
||||
: nothing
|
||||
}
|
||||
</div>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
showDialog({ hass, message }) {
|
||||
this.hass = hass;
|
||||
this._errorMsg = undefined;
|
||||
this._currentMessage = message;
|
||||
this._opened = true;
|
||||
const platform = message.platform;
|
||||
if (platform.has_media) {
|
||||
this._loading = true;
|
||||
const url = `/api/mailbox/media/${platform.name}/${message.sha}`;
|
||||
this.hass
|
||||
.fetchWithAuth(url)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.blob();
|
||||
}
|
||||
return Promise.reject({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
})
|
||||
.then((blob) => {
|
||||
this._loading = false;
|
||||
this._blobUrl = window.URL.createObjectURL(blob);
|
||||
})
|
||||
.catch((err) => {
|
||||
this._loading = false;
|
||||
this._errorMsg = `Error loading audio: ${err.statusText}`;
|
||||
});
|
||||
} else {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _openDeleteDialog() {
|
||||
if (confirm(this.hass.localize("ui.panel.mailbox.delete_prompt"))) {
|
||||
this._deleteSelected();
|
||||
}
|
||||
}
|
||||
|
||||
private _deleteSelected() {
|
||||
const msg = this._currentMessage;
|
||||
this.hass.callApi(
|
||||
"DELETE",
|
||||
`mailbox/delete/${msg.platform.name}/${msg.sha}`
|
||||
);
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
const mp3 = this.shadowRoot!.querySelector("#mp3")! as any;
|
||||
mp3.pause();
|
||||
this._currentMessage = undefined;
|
||||
this._errorMsg = undefined;
|
||||
this._loading = false;
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
text-align: var(--float-end);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-show-audio-message": HaDialogShowAudioMessage;
|
||||
}
|
||||
}
|
@@ -1,251 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-tabs";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import "../../styles/polymer-ha-style";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-tabs {
|
||||
margin-left: max(env(safe-area-inset-left), 24px);
|
||||
margin-right: max(env(safe-area-inset-right), 24px);
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
.content {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.date {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-app-layout>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
></ha-menu-button>
|
||||
<div main-title>[[localize('panel.mailbox')]]</div>
|
||||
</app-toolbar>
|
||||
<div sticky hidden$="[[areTabsHidden(platforms)]]">
|
||||
<ha-tabs
|
||||
scrollable
|
||||
selected="[[_currentPlatform]]"
|
||||
on-iron-activate="handlePlatformSelected"
|
||||
>
|
||||
<template is="dom-repeat" items="[[platforms]]">
|
||||
<paper-tab data-entity="[[item]]">
|
||||
[[getPlatformName(item)]]
|
||||
</paper-tab>
|
||||
</template>
|
||||
</ha-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<template is="dom-if" if="[[!_messages.length]]">
|
||||
<div class="card-content empty">
|
||||
[[localize('ui.panel.mailbox.empty')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[_messages]]">
|
||||
<paper-item on-click="openMP3Dialog">
|
||||
<paper-item-body style="width:100%" two-line>
|
||||
<div class="row">
|
||||
<div>[[item.caller]]</div>
|
||||
<div class="tip">
|
||||
[[localize('ui.duration.second', 'count', item.duration)]]
|
||||
</div>
|
||||
</div>
|
||||
<div secondary>
|
||||
<span class="date">[[item.timestamp]]</span> -
|
||||
[[item.message]]
|
||||
</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
</template>
|
||||
</ha-card>
|
||||
</div>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
|
||||
platforms: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
_messages: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
_currentPlatform: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
this.fire("register-dialog", {
|
||||
dialogShowEvent: "show-audio-message-dialog",
|
||||
dialogTag: "ha-dialog-show-audio-message",
|
||||
dialogImport: () => import("./ha-dialog-show-audio-message"),
|
||||
});
|
||||
}
|
||||
this.hassChanged = this.hassChanged.bind(this);
|
||||
this.hass.connection
|
||||
.subscribeEvents(this.hassChanged, "mailbox_updated")
|
||||
.then((unsub) => {
|
||||
this._unsubEvents = unsub;
|
||||
});
|
||||
this.computePlatforms().then((platforms) => {
|
||||
this.platforms = platforms;
|
||||
this.hassChanged();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubEvents) this._unsubEvents();
|
||||
}
|
||||
|
||||
hassChanged() {
|
||||
if (!this._messages) {
|
||||
this._messages = [];
|
||||
}
|
||||
this.getMessages().then((items) => {
|
||||
this._messages = items;
|
||||
});
|
||||
}
|
||||
|
||||
openMP3Dialog(event) {
|
||||
this.fire("show-audio-message-dialog", {
|
||||
hass: this.hass,
|
||||
message: event.model.item,
|
||||
});
|
||||
}
|
||||
|
||||
getMessages() {
|
||||
const platform = this.platforms[this._currentPlatform];
|
||||
return this.hass
|
||||
.callApi("GET", `mailbox/messages/${platform.name}`)
|
||||
.then((values) => {
|
||||
const platformItems = [];
|
||||
const arrayLength = values.length;
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
const datetime = formatDateTime(
|
||||
new Date(values[i].info.origtime * 1000),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
platformItems.push({
|
||||
timestamp: datetime,
|
||||
caller: values[i].info.callerid,
|
||||
message: values[i].text,
|
||||
sha: values[i].sha,
|
||||
duration: values[i].info.duration,
|
||||
platform: platform,
|
||||
});
|
||||
}
|
||||
return platformItems.sort(
|
||||
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
computePlatforms() {
|
||||
return this.hass.callApi("GET", "mailbox/platforms");
|
||||
}
|
||||
|
||||
handlePlatformSelected(ev) {
|
||||
const newPlatform = ev.detail.selected;
|
||||
if (newPlatform !== this._currentPlatform) {
|
||||
this._currentPlatform = newPlatform;
|
||||
this.hassChanged();
|
||||
}
|
||||
}
|
||||
|
||||
areTabsHidden(platforms) {
|
||||
return !platforms || platforms.length < 2;
|
||||
}
|
||||
|
||||
getPlatformName(item) {
|
||||
const entity = `mailbox.${item.name}`;
|
||||
const stateObj = this.hass.states[entity.toLowerCase()];
|
||||
return stateObj.attributes.friendly_name;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-panel-mailbox", HaPanelMailbox);
|
281
src/panels/mailbox/ha-panel-mailbox.ts
Normal file
281
src/panels/mailbox/ha-panel-mailbox.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "@material/mwc-button";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-tabs";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
interface MailboxMessage {
|
||||
info: {
|
||||
origtime: number;
|
||||
callerid: string;
|
||||
duration: string;
|
||||
};
|
||||
text: string;
|
||||
sha: string;
|
||||
}
|
||||
|
||||
@customElement("ha-panel-mailbox")
|
||||
class HaPanelMailbox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public platforms?: any[];
|
||||
|
||||
@state() private _messages?: any[];
|
||||
|
||||
@state() private _currentPlatform: number = 0;
|
||||
|
||||
private _unsubEvents?;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed>
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div slot="title">${this.hass.localize("panel.mailbox")}</div>
|
||||
${!this._areTabsHidden(this.platforms)
|
||||
? html`<div sticky>
|
||||
<ha-tabs
|
||||
scrollable
|
||||
.selected=${this._currentPlatform}
|
||||
@iron-activate=${this._handlePlatformSelected}
|
||||
>
|
||||
${this.platforms?.map(
|
||||
(platform) =>
|
||||
html` <paper-tab data-entity=${platform}>
|
||||
${this._getPlatformName(platform)}
|
||||
</paper-tab>`
|
||||
)}
|
||||
</ha-tabs>
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
${!this._messages?.length
|
||||
? html`<div class="card-content empty">
|
||||
${this.hass.localize("ui.panel.mailbox.empty")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._messages?.map(
|
||||
(message) =>
|
||||
html` <paper-item
|
||||
.message=${message}
|
||||
@click=${this._openMP3Dialog}
|
||||
>
|
||||
<paper-item-body style="width:100%" two-line>
|
||||
<div class="row">
|
||||
<div>${message.caller}</div>
|
||||
<div class="tip">
|
||||
${this.hass.localize(
|
||||
"ui.duration.second",
|
||||
"count",
|
||||
message.duration
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div secondary>
|
||||
<span class="date">${message.timestamp}</span> -
|
||||
${message.message}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
</paper-item>`
|
||||
)}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "show-audio-message-dialog",
|
||||
dialogTag: "ha-dialog-show-audio-message",
|
||||
dialogImport: () => import("./ha-dialog-show-audio-message"),
|
||||
});
|
||||
}
|
||||
this.hassChanged = this.hassChanged.bind(this);
|
||||
this.hass.connection
|
||||
.subscribeEvents(this.hassChanged, "mailbox_updated")
|
||||
.then((unsub) => {
|
||||
this._unsubEvents = unsub;
|
||||
});
|
||||
this._computePlatforms().then((platforms) => {
|
||||
this.platforms = platforms;
|
||||
this.hassChanged();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubEvents) this._unsubEvents();
|
||||
}
|
||||
|
||||
hassChanged() {
|
||||
if (!this._messages) {
|
||||
this._messages = [];
|
||||
}
|
||||
this._getMessages().then((items) => {
|
||||
this._messages = items;
|
||||
});
|
||||
}
|
||||
|
||||
private _openMP3Dialog(ev) {
|
||||
const message: any = (ev.currentTarget! as any).message;
|
||||
fireEvent(this, "show-audio-message-dialog", {
|
||||
hass: this.hass,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
private _getMessages() {
|
||||
const platform = this.platforms![this._currentPlatform];
|
||||
return this.hass
|
||||
.callApi<MailboxMessage[]>("GET", `mailbox/messages/${platform.name}`)
|
||||
.then((values) => {
|
||||
const platformItems: any[] = [];
|
||||
const arrayLength = values.length;
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
const datetime = formatDateTime(
|
||||
new Date(values[i].info.origtime * 1000),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
platformItems.push({
|
||||
timestamp: datetime,
|
||||
caller: values[i].info.callerid,
|
||||
message: values[i].text,
|
||||
sha: values[i].sha,
|
||||
duration: values[i].info.duration,
|
||||
platform: platform,
|
||||
});
|
||||
}
|
||||
return platformItems.sort((a, b) => b.timestamp - a.timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
private _computePlatforms(): Promise<any[]> {
|
||||
return this.hass.callApi<any[]>("GET", "mailbox/platforms");
|
||||
}
|
||||
|
||||
private _handlePlatformSelected(ev) {
|
||||
const newPlatform = ev.detail.selected;
|
||||
if (newPlatform !== this._currentPlatform) {
|
||||
this._currentPlatform = newPlatform;
|
||||
this.hassChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _areTabsHidden(platforms) {
|
||||
return !platforms || platforms.length < 2;
|
||||
}
|
||||
|
||||
private _getPlatformName(item) {
|
||||
const entity = `mailbox.${item.name}`;
|
||||
const stateObj = this.hass.states[entity.toLowerCase()];
|
||||
return stateObj.attributes.friendly_name;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-tabs {
|
||||
margin-left: max(env(safe-area-inset-left), 24px);
|
||||
margin-right: max(env(safe-area-inset-right), 24px);
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
.content {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.date {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-mailbox": HaPanelMailbox;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"show-audio-message-dialog": {
|
||||
hass: HomeAssistant;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
}
|
@@ -299,6 +299,7 @@ export const haStyleDialog = css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 400px;
|
||||
--mdc-dialog-max-width: 600px;
|
||||
--mdc-dialog-max-width: min(600px, 95vw);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
|
||||
|
@@ -15,8 +15,6 @@
|
||||
},
|
||||
"state": {
|
||||
"default": {
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"unknown": "Unknown",
|
||||
"unavailable": "Unavailable"
|
||||
}
|
||||
@@ -107,8 +105,7 @@
|
||||
"cooling": "{name} cooling",
|
||||
"high": "high",
|
||||
"low": "low",
|
||||
"operation": "Operation",
|
||||
"away_mode": "Away mode"
|
||||
"mode": "Mode"
|
||||
},
|
||||
"counter": {
|
||||
"actions": {
|
||||
@@ -228,8 +225,8 @@
|
||||
"currently": "Currently",
|
||||
"on_off": "On / off",
|
||||
"target_temperature": "Target temperature",
|
||||
"operation": "Operation",
|
||||
"away_mode": "Away mode"
|
||||
"away_mode": "Away mode",
|
||||
"mode": "Mode"
|
||||
},
|
||||
"weather": {
|
||||
"attributes": {
|
||||
@@ -318,6 +315,10 @@
|
||||
"manual": "Manually enter media ID",
|
||||
"media_content_id": "Media content ID",
|
||||
"media_content_type": "Media content type"
|
||||
},
|
||||
"file": {
|
||||
"upload_failed": "Upload failed",
|
||||
"unknown_file": "Unknown file"
|
||||
}
|
||||
},
|
||||
"logbook": {
|
||||
@@ -498,8 +499,18 @@
|
||||
"filtered_by_device": "device: {device_name}",
|
||||
"filtered_by_area": "area: {area_name}"
|
||||
},
|
||||
"file-upload": {
|
||||
"uploading": "Uploading...",
|
||||
"uploading_name": "Uploading {name}",
|
||||
"label": "Add file",
|
||||
"secondary": "Or drop your file here",
|
||||
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
|
||||
},
|
||||
"picture-upload": {
|
||||
"label": "Picture",
|
||||
"label": "Add picture",
|
||||
"change_picture": "Change picture",
|
||||
"current_image_alt": "Current picture",
|
||||
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
||||
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
|
||||
},
|
||||
"date-range-picker": {
|
||||
@@ -1007,7 +1018,9 @@
|
||||
"climate": {
|
||||
"target_label": "{action} to target",
|
||||
"target": "Target",
|
||||
"humidity_target": "Humidity target"
|
||||
"humidity_target": "Humidity target",
|
||||
"temperature": "Temperature",
|
||||
"humidity": "Humidity"
|
||||
},
|
||||
"humidifier": {
|
||||
"target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]",
|
||||
@@ -3946,7 +3959,6 @@
|
||||
"device_info": {
|
||||
"zwave_info": "Z-Wave info",
|
||||
"node_id": "ID",
|
||||
"node_status": "Status",
|
||||
"node_ready": "Ready",
|
||||
"device_config": "Configure",
|
||||
"reinterview_device": "Re-interview",
|
||||
@@ -4053,13 +4065,6 @@
|
||||
"parameter": "Parameter",
|
||||
"bitmask": "Bitmask"
|
||||
},
|
||||
"node_status": {
|
||||
"unknown": "Unknown",
|
||||
"asleep": "Asleep",
|
||||
"awake": "Awake",
|
||||
"dead": "Dead",
|
||||
"alive": "Alive"
|
||||
},
|
||||
"network_status": {
|
||||
"connected": "Connected",
|
||||
"connecting": "Connecting",
|
||||
@@ -5687,7 +5692,21 @@
|
||||
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
|
||||
"next": "Next",
|
||||
"finish": "Finish",
|
||||
"help": "Help",
|
||||
"welcome": {
|
||||
"header": "Welcome!",
|
||||
"start": "Create my smart home",
|
||||
"restore_backup": "Restore from backup",
|
||||
"vision": "Read our vision",
|
||||
"community": "Join our community",
|
||||
"download_app": "Download our app",
|
||||
"forums": "Home Assistant forums",
|
||||
"open_home_newsletter": "Building the Open Home newsletter",
|
||||
"discord": "Discord chat",
|
||||
"twitter": "Twitter"
|
||||
},
|
||||
"user": {
|
||||
"header": "Create user",
|
||||
"intro": "Let's get started by creating a user account.",
|
||||
"required_field": "Required",
|
||||
"data": {
|
||||
@@ -5696,20 +5715,22 @@
|
||||
"password": "Password",
|
||||
"password_confirm": "Confirm password"
|
||||
},
|
||||
"helper": {
|
||||
"password": "Choose a strong and unique password. Make sure to save it, so you don't forget it."
|
||||
},
|
||||
"create_account": "Create account",
|
||||
"error": {
|
||||
"password_not_match": "Passwords don't match"
|
||||
}
|
||||
},
|
||||
"core-config": {
|
||||
"intro": "Hello {name}, welcome to Home Assistant. How would you like to name your home?",
|
||||
"intro_core": "We will set up the basics together. You can always change this later in the settings.",
|
||||
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations. This data is never shared outside of your network.",
|
||||
"location_header": "Home location",
|
||||
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations.",
|
||||
"location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).",
|
||||
"osm_privacy_policy": "Privacy policy",
|
||||
"title_location_detect": "Do you want us to detect your location?",
|
||||
"intro_location_detect": "We can detect your location by making a one-time request to an external service.",
|
||||
"intro_core_config": "We filled out some details about your location. Please check if they are correct and continue.",
|
||||
"country_intro": "We would like to know the country your home is in, so we can use the correct units for you.",
|
||||
"location_name": "Name of your Home Assistant installation",
|
||||
"location_name_default": "Home",
|
||||
"address_label": "Search address",
|
||||
@@ -5717,11 +5738,13 @@
|
||||
"finish": "Next"
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Devices and services are represented in Home Assistant as integrations. You can set them up now, or do it later from the settings.",
|
||||
"more_integrations": "More",
|
||||
"header": "We found compatible devices!",
|
||||
"intro": "These are found on your local network. Some are already added, others may need extra configuration.",
|
||||
"more_integrations": "+{count} more",
|
||||
"finish": "Finish"
|
||||
},
|
||||
"analytics": {
|
||||
"header": "Help us help you",
|
||||
"finish": "Next",
|
||||
"preferences": {
|
||||
"base": {
|
||||
@@ -5746,8 +5769,9 @@
|
||||
"intro": "[%key:ui::panel::config::analytics::intro%]"
|
||||
},
|
||||
"restore": {
|
||||
"description": "Alternatively you can restore from a previous backup.",
|
||||
"header": "Restore a backup",
|
||||
"in_progress": "Restore in progress",
|
||||
"upload_backup": "[%key:supervisor::backup::upload_backup%]",
|
||||
"show_log": "Show full log",
|
||||
"hide_log": "Hide full log",
|
||||
"full_backup": "[%key:supervisor::backup::full_backup%]",
|
||||
@@ -5759,8 +5783,7 @@
|
||||
"addons": "[%key:supervisor::backup::addons%]",
|
||||
"password_protection": "[%key:supervisor::backup::password_protection%]",
|
||||
"password": "[%key:supervisor::backup::password%]",
|
||||
"confirm_password": "[%key:supervisor::backup::confirm_password%]",
|
||||
"upload_backup": "[%key:supervisor::backup::upload_backup%]"
|
||||
"confirm_password": "[%key:supervisor::backup::confirm_password%]"
|
||||
}
|
||||
},
|
||||
"custom": {
|
||||
|
111
yarn.lock
111
yarn.lock
@@ -1454,15 +1454,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/commands@npm:6.2.4":
|
||||
version: 6.2.4
|
||||
resolution: "@codemirror/commands@npm:6.2.4"
|
||||
"@codemirror/commands@npm:6.2.5":
|
||||
version: 6.2.5
|
||||
resolution: "@codemirror/commands@npm:6.2.5"
|
||||
dependencies:
|
||||
"@codemirror/language": ^6.0.0
|
||||
"@codemirror/state": ^6.2.0
|
||||
"@codemirror/view": ^6.0.0
|
||||
"@lezer/common": ^1.0.0
|
||||
checksum: 468895fa19ff0554181b698c81f850820de5c0289cab92c44392fb127286f09ca72b921d6ea4353b70b616a4fd0c3667d86b6f917202a3ad2e196eb7b581f7b6
|
||||
checksum: 6d373bcfd4337160243e1493c8703a8e367e208811742331679a6410a3645de36ae8a5664e11790fec521137b45f34d703e9292932a98c4de10139510f3f29a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1489,14 +1489,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/search@npm:6.5.1":
|
||||
version: 6.5.1
|
||||
resolution: "@codemirror/search@npm:6.5.1"
|
||||
"@codemirror/search@npm:6.5.2":
|
||||
version: 6.5.2
|
||||
resolution: "@codemirror/search@npm:6.5.2"
|
||||
dependencies:
|
||||
"@codemirror/state": ^6.0.0
|
||||
"@codemirror/view": ^6.0.0
|
||||
crelt: ^1.0.5
|
||||
checksum: 672515c20238f69ff5cd8b662128699178ba7e020fc44a8ed2b0dcc25d8d5f5579418865616dd8809317a408fb08b6001a442f0fb706a772250b4284d7437045
|
||||
checksum: bc535151277fda0a370ac496b9b0d5751fd91bd8e3eb29dafbfe6bf3125dc450a7e361ebc302f0ebc4193ac337bdf555ab3d5ec753dbb44452225618a5630dd3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1578,10 +1578,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:^8.47.0":
|
||||
version: 8.47.0
|
||||
resolution: "@eslint/js@npm:8.47.0"
|
||||
checksum: 0ef57fe27b6d4c305b33f3b2d2fee1ab397a619006f1d6f4ce5ee4746b8f03d11a4e098805a7d78601ca534cf72917d37f0ac19896c992a32e26299ecb9f9de1
|
||||
"@eslint/js@npm:8.48.0":
|
||||
version: 8.48.0
|
||||
resolution: "@eslint/js@npm:8.48.0"
|
||||
checksum: b2755f9c0ee810c886eba3c50dcacb184ba5a5cd1cbc01988ee506ad7340653cae0bd55f1d95c64b56dfc6d25c2caa7825335ffd2c50165bae9996fe0f396851
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2039,6 +2039,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@kurkle/color@npm:^0.3.0":
|
||||
version: 0.3.2
|
||||
resolution: "@kurkle/color@npm:0.3.2"
|
||||
checksum: 79e97b31f8f6efb28c69d373f94b0c7480226fe8ec95221f518ac998e156444a496727ce47de6d728eb5c3369288e794cba82cae34253deb0d472d3bfe080e49
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@leichtgewicht/ip-codec@npm:^2.0.1":
|
||||
version: 2.0.4
|
||||
resolution: "@leichtgewicht/ip-codec@npm:2.0.4"
|
||||
@@ -3411,19 +3418,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/app-layout@npm:3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@polymer/app-layout@npm:3.1.0"
|
||||
dependencies:
|
||||
"@polymer/iron-flex-layout": ^3.0.0-pre.26
|
||||
"@polymer/iron-media-query": ^3.0.0-pre.26
|
||||
"@polymer/iron-resizable-behavior": ^3.0.0-pre.26
|
||||
"@polymer/iron-scroll-target-behavior": ^3.0.0-pre.26
|
||||
"@polymer/polymer": ^3.0.0
|
||||
checksum: 0da7158de7a44db6f25482ef28a75a4a9372706b0d6a0d08f7b45eb4f75d60dd2e769275bf926f986588bc8273545b55f90a68429c6081cb5cb3f16b1e1f9746
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/font-roboto@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "@polymer/font-roboto@npm:3.0.2"
|
||||
@@ -3541,15 +3535,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/iron-media-query@npm:^3.0.0-pre.26":
|
||||
version: 3.0.1
|
||||
resolution: "@polymer/iron-media-query@npm:3.0.1"
|
||||
dependencies:
|
||||
"@polymer/polymer": ^3.0.0
|
||||
checksum: 15d7c77608925adea02bf12af1f1eb7dd70abc3349bdff88906a3f0fa5bdfe41171bda1b346f9b7ccfd6de9550ac2a34278a74182ce1407097d3972c23984c5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/iron-menu-behavior@npm:^3.0.0-pre.26":
|
||||
version: 3.0.2
|
||||
resolution: "@polymer/iron-menu-behavior@npm:3.0.2"
|
||||
@@ -3601,15 +3586,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/iron-scroll-target-behavior@npm:^3.0.0-pre.26":
|
||||
version: 3.0.1
|
||||
resolution: "@polymer/iron-scroll-target-behavior@npm:3.0.1"
|
||||
dependencies:
|
||||
"@polymer/polymer": ^3.0.0
|
||||
checksum: abf2864aee6049b336aa303110a65cd51cda75247b89edc4f21a6b5290062eb19cd8d2d125198354497df53368bbd305b30d9d31475fd327833cd549d83eb7b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polymer/iron-selector@npm:^3.0.0-pre.26":
|
||||
version: 3.0.1
|
||||
resolution: "@polymer/iron-selector@npm:3.0.1"
|
||||
@@ -6570,10 +6546,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chart.js@npm:3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "chart.js@npm:3.3.2"
|
||||
checksum: 0aaebab52cb5eacfc969210f7c304cad7e20b03a37b33f4e0332b5eeb9319e2929b085e0a8654ec33752566a55982ffe6bb3f2ba620c2a9b3bc71806ca7b9cb7
|
||||
"chart.js@npm:4.3.3":
|
||||
version: 4.3.3
|
||||
resolution: "chart.js@npm:4.3.3"
|
||||
dependencies:
|
||||
"@kurkle/color": ^0.3.0
|
||||
checksum: 548605fc0a0a64fdc591a41159a2be86c1b408339d6e57b566c04dc157331b003db285a6139801d1700596acde8f0521bc6a156da29388025581619db6600073
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7141,10 +7119,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cropperjs@npm:1.5.13":
|
||||
version: 1.5.13
|
||||
resolution: "cropperjs@npm:1.5.13"
|
||||
checksum: 5a3d0f8b2aa41eb711371696ccbac040d7ef81a5fc9cc0304aaf5691f9aa5c243b539c3d8aec0f938e8fa11e73507ae082875b262ff7fd069892acfcd5d5b567
|
||||
"cropperjs@npm:1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "cropperjs@npm:1.6.0"
|
||||
checksum: 643d1e939335ea7426746f03b43ca05848867b095930ea6c84b141ffbe1962b7d5e20e5e5807f19879b95936255ca1fc8823553ffb115e84dca6578876da8b11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8158,14 +8136,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:8.47.0":
|
||||
version: 8.47.0
|
||||
resolution: "eslint@npm:8.47.0"
|
||||
"eslint@npm:8.48.0":
|
||||
version: 8.48.0
|
||||
resolution: "eslint@npm:8.48.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": ^4.2.0
|
||||
"@eslint-community/regexpp": ^4.6.1
|
||||
"@eslint/eslintrc": ^2.1.2
|
||||
"@eslint/js": ^8.47.0
|
||||
"@eslint/js": 8.48.0
|
||||
"@humanwhocodes/config-array": ^0.11.10
|
||||
"@humanwhocodes/module-importer": ^1.0.1
|
||||
"@nodelib/fs.walk": ^1.2.8
|
||||
@@ -8201,7 +8179,7 @@ __metadata:
|
||||
text-table: ^0.2.0
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 1988617f703eadc5c7540468d54dc8e5171cf2bb9483f6172799cd1ff54a9a5e4470f003784e8cef92687eaa14de37172732787040e67817581a20bcb9c15970
|
||||
checksum: f20b359a4f8123fec5c033577368cc020d42978b1b45303974acd8da7a27063168ee3fe297ab5b35327162f6a93154063e3ce6577102f70f9809aff793db9bd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9611,10 +9589,10 @@ __metadata:
|
||||
"@babel/runtime": 7.22.11
|
||||
"@braintree/sanitize-url": 6.0.4
|
||||
"@codemirror/autocomplete": 6.9.0
|
||||
"@codemirror/commands": 6.2.4
|
||||
"@codemirror/commands": 6.2.5
|
||||
"@codemirror/language": 6.9.0
|
||||
"@codemirror/legacy-modes": 6.3.3
|
||||
"@codemirror/search": 6.5.1
|
||||
"@codemirror/search": 6.5.2
|
||||
"@codemirror/state": 6.2.1
|
||||
"@codemirror/view": 6.16.0
|
||||
"@egjs/hammerjs": 2.0.17
|
||||
@@ -9670,7 +9648,6 @@ __metadata:
|
||||
"@octokit/plugin-retry": 6.0.0
|
||||
"@octokit/rest": 20.0.1
|
||||
"@open-wc/dev-server-hmr": 0.1.4
|
||||
"@polymer/app-layout": 3.1.0
|
||||
"@polymer/iron-flex-layout": 3.0.1
|
||||
"@polymer/iron-input": 3.0.1
|
||||
"@polymer/iron-resizable-behavior": 3.0.1
|
||||
@@ -9720,16 +9697,16 @@ __metadata:
|
||||
babel-loader: 9.1.3
|
||||
babel-plugin-template-html-minifier: 4.1.0
|
||||
chai: 4.3.8
|
||||
chart.js: 3.3.2
|
||||
chart.js: 4.3.3
|
||||
comlink: 4.4.1
|
||||
core-js: 3.32.1
|
||||
cropperjs: 1.5.13
|
||||
cropperjs: 1.6.0
|
||||
date-fns: 2.30.0
|
||||
date-fns-tz: 2.0.0
|
||||
deep-clone-simple: 1.1.1
|
||||
deep-freeze: 0.0.1
|
||||
del: 7.0.0
|
||||
eslint: 8.47.0
|
||||
eslint: 8.48.0
|
||||
eslint-config-airbnb-base: 15.0.0
|
||||
eslint-config-airbnb-typescript: 17.1.0
|
||||
eslint-config-prettier: 9.0.0
|
||||
@@ -9770,7 +9747,7 @@ __metadata:
|
||||
luxon: 3.4.2
|
||||
magic-string: 0.30.3
|
||||
map-stream: 0.0.7
|
||||
marked: 7.0.4
|
||||
marked: 7.0.5
|
||||
memoize-one: 6.0.0
|
||||
mocha: 10.2.0
|
||||
node-vibrant: 3.2.1-alpha.1
|
||||
@@ -11751,12 +11728,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:7.0.4":
|
||||
version: 7.0.4
|
||||
resolution: "marked@npm:7.0.4"
|
||||
"marked@npm:7.0.5":
|
||||
version: 7.0.5
|
||||
resolution: "marked@npm:7.0.5"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: e69cc5635d13aec3486340ee97b059a360c429cbbdbdbdf6d23cdf79b6f5e5edc3f3d649763b3482261705aa91e5f08b14f91dd85be1452a98b65230485d7f55
|
||||
checksum: e136ab58cffae78e9e661136e8ba51c221aed1a4b7e60dcb9ae7d7241e87869e972b3d352ca3b9b7f56dba85709f848414d33060c4d0e88b3b80e01a3509e2ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user