Start updating styling of onboarding (#17698)

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Bram Kragten 2023-08-30 12:37:14 +02:00
parent a8e17da9f3
commit fe91dbb139
27 changed files with 7747 additions and 632 deletions

View File

@ -173,6 +173,7 @@ class HassioBackupDialog
private async _restoreClicked() { private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails(); const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true; this._restoringBackup = true;
this._dialogParams?.onRestoring?.();
if (this._backupContent.backupType === "full") { if (this._backupContent.backupType === "full") {
await this._fullRestoreClicked(backupDetails); await this._fullRestoreClicked(backupDetails);
} else { } else {

View File

@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams { export interface HassioBackupDialogParams {
slug: string; slug: string;
onDelete?: () => void; onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean; onboarding?: boolean;
supervisor?: Supervisor; supervisor?: Supervisor;
localize?: LocalizeFunc; localize?: LocalizeFunc;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -10,12 +10,12 @@ import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"]; const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = ( export const createCloseHeading = (
hass: HomeAssistant, hass: HomeAssistant | undefined,
title: string | TemplateResult title: string | TemplateResult
) => html` ) => html`
<div class="header_title">${title}</div> <div class="header_title">${title}</div>
<ha-icon-button <ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")} .label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"

View File

@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation"; import { FrontendLocaleData } from "../data/translation";
import "../resources/intl-polyfill"; import "../resources/intl-polyfill";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
@property() public languages?: string[]; @property() public languages?: string[];
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
@ -62,8 +63,8 @@ export class HaLanguagePicker extends LitElement {
} }
const languageOptions = this._getLanguagesOptions( const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages, this.languages ?? this._defaultLanguages,
this.hass.locale, this.nativeName,
this.nativeName this.hass?.locale
); );
const selectedItemIndex = languageOptions.findIndex( const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value (option) => option.value === this.value
@ -78,11 +79,11 @@ export class HaLanguagePicker extends LitElement {
} }
private _getLanguagesOptions = memoizeOne( private _getLanguagesOptions = memoizeOne(
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => { (languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
let options: { label: string; value: string }[] = []; let options: { label: string; value: string }[] = [];
if (nativeName) { if (nativeName) {
const translations = this.hass.translationMetadata.translations; const translations = translationMetadata.translations;
options = languages.map((lang) => { options = languages.map((lang) => {
let label = translations[lang]?.nativeName; let label = translations[lang]?.nativeName;
if (!label) { if (!label) {
@ -101,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
label, label,
}; };
}); });
} else { } else if (locale) {
options = languages.map((lang) => ({ options = languages.map((lang) => ({
value: lang, value: lang,
label: formatLanguageCode(lang, locale), label: formatLanguageCode(lang, locale),
})); }));
} }
if (!this.noSort) { if (!this.noSort && locale) {
options.sort((a, b) => options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language) caseInsensitiveStringCompare(a.label, b.label, locale.language)
); );
@ -118,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
); );
private _computeDefaultLanguageOptions() { private _computeDefaultLanguageOptions() {
if (!this.hass.translationMetadata?.translations) { this._defaultLanguages = Object.keys(translationMetadata.translations);
return;
}
this._defaultLanguages = Object.keys(
this.hass.translationMetadata.translations
);
} }
protected render() { protected render() {
const languageOptions = this._getLanguagesOptions( const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages, this.languages ?? this._defaultLanguages,
this.hass.locale, this.nativeName,
this.nativeName this.hass?.locale
); );
const value = const value =
@ -139,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ??
this.hass.localize("ui.components.language-picker.language")} (this.hass?.localize("ui.components.language-picker.language") ||
.value=${value} "Language")}
.value=${value || ""}
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
@selected=${this._changed} @selected=${this._changed}
@ -151,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
> >
${languageOptions.length === 0 ${languageOptions.length === 0
? html`<ha-list-item value="" ? html`<ha-list-item value=""
>${this.hass.localize( >${this.hass?.localize(
"ui.components.language-picker.no_languages" "ui.components.language-picker.no_languages"
)}</ha-list-item ) || "No languages"}</ha-list-item
>` >`
: languageOptions.map( : languageOptions.map(
(option) => html` (option) => html`
@ -176,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
private _changed(ev): void { private _changed(ev): void {
const target = ev.target as HaSelect; const target = ev.target as HaSelect;
if (!this.hass || target.value === "" || target.value === this.value) { if (target.value === "" || target.value === this.value) {
return; return;
} }
this.value = target.value; this.value = target.value;

View File

@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor { .mdc-select__anchor {
width: var(--ha-select-min-width, 200px); width: var(--ha-select-min-width, 200px);
} }
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label { .mdc-select--filled .mdc-floating-label {
inset-inline-start: 12px; inset-inline-start: 12px;
inset-inline-end: initial; inset-inline-end: initial;

View File

@ -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;
};

View File

@ -6,44 +6,40 @@
<%= renderTemplate("_style_base.html.template") %> <%= renderTemplate("_style_base.html.template") %>
<style> <style>
html { html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121); 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 { body {
height: auto; height: auto;
padding: 64px 0; padding: 32px 0;
} }
.content { .content {
max-width: 432px; max-width: 560px;
margin: 0 auto; margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
} }
.header { .header {
text-align: center;
font-size: 1.96em;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: 300; margin-bottom: 32px;
color: var(--text-primary-color, #fff);
margin-bottom: 16px;
} }
.header img { .header img {
margin-right: 16px; height: 56px;
width: 56px;
} }
@media (prefers-color-scheme: dark) { @media (max-width: 592px) {
html {
color: #e1e1e1;
}
}
@media (max-width: 450px) {
.content { .content {
min-height: 100%; margin: 0 16px;
margin: 0;
} }
} }
</style> </style>
@ -51,8 +47,7 @@
<body id="particles"> <body id="particles">
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" /> <img src="/static/icons/favicon-192x192.png" alt="Home Assistant" />
Home Assistant
</div> </div>
<ha-onboarding></ha-onboarding> <ha-onboarding></ha-onboarding>
</div> </div>

File diff suppressed because one or more lines are too long

View 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;
}
}

View 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,
});
};

View 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,
});
};

View File

@ -1,3 +1,4 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { import {
Auth, Auth,
createConnection, createConnection,
@ -5,38 +6,45 @@ import {
getAuth, getAuth,
subscribeConfig, subscribeConfig,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { html, PropertyValues, nothing, css } from "lit"; import { PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; 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 { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { HASSDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent } from "../common/dom/fire_event";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
import "../components/ha-card";
import "../components/ha-language-picker";
import { AuthUrlSearchParams, hassUrl } from "../data/auth"; import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import { import {
fetchInstallationType,
fetchOnboardingOverview,
OnboardingResponses, OnboardingResponses,
OnboardingStep, OnboardingStep,
fetchInstallationType,
fetchOnboardingOverview,
onboardIntegrationStep, onboardIntegrationStep,
} from "../data/onboarding"; } from "../data/onboarding";
import { subscribeUser } from "../data/ws-user"; import { subscribeUser } from "../data/ws-user";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { HassElement } from "../state/hass-element"; import { HassElement } from "../state/hass-element";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import "./onboarding-analytics"; import "./onboarding-analytics";
import "./onboarding-create-user"; import "./onboarding-create-user";
import "./onboarding-loading"; import "./onboarding-loading";
import "../components/ha-language-picker"; import "./onboarding-welcome";
import "../components/ha-card"; import "./onboarding-welcome-links";
import { storeState } from "../util/ha-pref-storage"; import { makeDialogManager } from "../dialogs/make-dialog-manager";
import {
enableWrite,
loadTokens,
saveTokens,
} from "../common/auth/token_storage";
type OnboardingEvent = type OnboardingEvent =
| {
type: "init";
result: { restore: boolean };
}
| { | {
type: "user"; type: "user";
result: OnboardingResponses["user"]; result: OnboardingResponses["user"];
@ -52,13 +60,21 @@ type OnboardingEvent =
type: "analytics"; type: "analytics";
}; };
interface OnboardingProgressEvent {
increase?: number;
decrease?: number;
progress?: number;
}
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"onboarding-step": OnboardingEvent; "onboarding-step": OnboardingEvent;
"onboarding-progress": OnboardingProgressEvent;
} }
interface GlobalEventHandlersEventMap { interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent>; "onboarding-step": HASSDomEvent<OnboardingEvent>;
"onboarding-progress": HASSDomEvent<OnboardingProgressEvent>;
} }
} }
@ -68,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@property() public translationFragment = "page-onboarding"; @property() public translationFragment = "page-onboarding";
@state() private _progress = 0;
@state() private _loading = false; @state() private _loading = false;
@state() private _init = false;
@state() private _restoring = false; @state() private _restoring = false;
@state() private _supervisor?: boolean; @state() private _supervisor?: boolean;
@ -77,44 +97,58 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[]; @state() private _steps?: OnboardingStep[];
protected render() { protected render() {
return html`<ha-card> return html`<mwc-linear-progress
.progress=${this._progress}
></mwc-linear-progress>
<ha-card>
<div class="card-content">${this._renderStep()}</div> <div class="card-content">${this._renderStep()}</div>
</ha-card> </ha-card>
${this.hass ${this._init
? html`<ha-language-picker ? html`<onboarding-welcome-links
.hass=${this.hass} .localize=${this.localize}
.value=${this.language} ></onboarding-welcome-links>`
.label=${this.localize("ui.panel.page-onboarding.language")} : nothing}
nativeName <div class="footer">
@value-changed=${this._languageChanged} <ha-language-picker
></ha-language-picker>` .value=${this.language}
: nothing} `; .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() { 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()!; const step = this._curStep()!;
if (this._loading || !step) { if (this._loading || !step) {
return html`<onboarding-loading></onboarding-loading> `; return html`<onboarding-loading></onboarding-loading> `;
} }
if (step.step === "user") { if (step.step === "user") {
return html` return html`<onboarding-create-user
${!this._restoring .localize=${this.localize}
? html`<onboarding-create-user .language=${this.language}
.localize=${this.localize} >
.language=${this.language} </onboarding-create-user>`;
>
</onboarding-create-user>`
: ""}
${this._supervisor
? html`<onboarding-restore-backup
.localize=${this.localize}
.restoring=${this._restoring}
@restoring=${this._restoringBackup}
>
</onboarding-restore-backup>`
: ""}
`;
} }
if (step.step === "core_config") { if (step.step === "core_config") {
return html` return html`
@ -150,9 +184,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./onboarding-core-config"); import("./onboarding-core-config");
registerServiceWorker(this, false); registerServiceWorker(this, false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev)); this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
this.addEventListener("onboarding-progress", (ev) =>
this._handleProgress(ev)
);
if (window.innerWidth > 450) { if (window.innerWidth > 450) {
import("./particles"); import("./particles");
} }
makeDialogManager(this, this.shadowRoot!);
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
@ -187,10 +225,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
return this._steps ? this._steps.find((stp) => !stp.done) : undefined; return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
} }
private _restoringBackup() {
this._restoring = true;
}
private async _fetchInstallationType(): Promise<void> { private async _fetchInstallationType(): Promise<void> {
try { try {
const response = await fetchInstallationType(); const response = await fetchInstallationType();
@ -239,8 +273,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}); });
history.replaceState(null, "", location.pathname); history.replaceState(null, "", location.pathname);
await this._connectHass(auth); await this._connectHass(auth);
const currentStep = steps.findIndex((stp) => !stp.done);
const singelStepProgress = 1 / steps.length;
this._progress = currentStep * singelStepProgress + singelStepProgress;
} else { } else {
// User creating screen needs to know the installation type. this._init = true;
// Init screen needs to know the installation type.
this._fetchInstallationType(); this._fetchInstallationType();
} }
@ -250,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>) { private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
const stepResult = ev.detail; const stepResult = ev.detail;
this._steps = this._steps!.map((step) => this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : 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"]; const result = stepResult.result as OnboardingResponses["user"];
this._loading = true; this._loading = true;
this._progress = 0.5;
enableWrite(); enableWrite();
try { try {
const auth = await getAuth({ const auth = await getAuth({
@ -275,6 +333,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._loading = false; this._loading = false;
} }
} else if (stepResult.type === "core_config") { } else if (stepResult.type === "core_config") {
this._progress = 0.75;
// We do nothing
} else if (stepResult.type === "analytics") {
this._progress = 1;
// We do nothing // We do nothing
} else if (stepResult.type === "integration") { } else if (stepResult.type === "integration") {
this._loading = true; this._loading = true;
@ -348,6 +410,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser), subscribeOne(conn, subscribeUser),
]); ]);
this.initializeHass(auth, conn); 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 // Load config strings for integrations
(this as any)._loadFragmentTranslations(this.hass!.language, "config"); (this as any)._loadFragmentTranslations(this.hass!.language, "config");
// Make sure hass is initialized + the config/user callbacks have called. // Make sure hass is initialized + the config/user callbacks have called.
@ -359,25 +429,54 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
private _languageChanged(ev: CustomEvent) { private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value; const language = ev.detail.value;
this.language = language; this.language = language;
this._updateHass({ if (this.hass) {
locale: { ...this.hass!.locale, language }, this._updateHass({
language, locale: { ...this.hass!.locale, language },
selectedLanguage: language, language,
}); selectedLanguage: language,
storeState(this.hass!); });
storeState(this.hass!);
} else {
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}
}
} }
static styles = css` 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 { ha-language-picker {
display: block; display: block;
width: 200px; width: 200px;
margin-top: 8px; margin-top: 8px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none; --mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--text-primary-color, #fff); --mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--text-primary-color, #fff); --mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: var(--text-primary-color, #fff); --mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: var(--text-primary-color, #fff); --mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--text-primary-color, #fff); --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;
} }
`; `;
} }

View File

@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiOpenInNew } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@ -8,6 +9,7 @@ import { Analytics, setAnalyticsPreferences } from "../data/analytics";
import { onboardAnalyticsStep } from "../data/onboarding"; import { onboardAnalyticsStep } from "../data/onboarding";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url"; import { documentationUrl } from "../util/documentation-url";
import { onBoardingStyles } from "./styles";
@customElement("onboarding-analytics") @customElement("onboarding-analytics")
class OnboardingAnalytics extends LitElement { class OnboardingAnalytics extends LitElement {
@ -23,7 +25,18 @@ class OnboardingAnalytics extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<h1>${this.localize("ui.panel.page-onboarding.analytics.header")}</h1>
<p>${this.localize("ui.panel.page-onboarding.analytics.intro")}</p> <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 <ha-analytics
translation_key_panel="page-onboarding" translation_key_panel="page-onboarding"
@analytics-preferences-changed=${this._preferencesChanged} @analytics-preferences-changed=${this._preferencesChanged}
@ -33,16 +46,13 @@ class OnboardingAnalytics extends LitElement {
</ha-analytics> </ha-analytics>
${this._error ? html`<div class="error">${this._error}</div>` : ""} ${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="footer"> <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")} ${this.localize("ui.panel.page-onboarding.analytics.finish")}
</mwc-button> </mwc-button>
<a
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
</a>
</div> </div>
`; `;
} }
@ -81,27 +91,19 @@ class OnboardingAnalytics extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
p { onBoardingStyles,
font-size: 14px; css`
line-height: 20px; .error {
} color: var(--error-color);
.error { }
color: var(--error-color); a {
} color: var(--primary-color);
.footer { text-decoration: none;
margin-top: 16px; --mdc-icon-size: 14px;
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
} }
} }

View File

@ -13,15 +13,6 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-country-picker"; 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 { ConfigUpdateValues, saveCoreConfig } from "../data/core";
import { countryCurrency } from "../data/currency"; import { countryCurrency } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding"; import { onboardCoreConfigStep } from "../data/onboarding";
@ -39,15 +30,16 @@ class OnboardingCoreConfig extends LitElement {
@state() private _location?: [number, number]; @state() private _location?: [number, number];
@state() private _elevation?: string; private _elevation = "0";
@state() private _unitSystem?: ConfigUpdateValues["unit_system"]; private _unitSystem: ConfigUpdateValues["unit_system"] = "metric";
@state() private _currency?: ConfigUpdateValues["currency"]; private _currency: ConfigUpdateValues["currency"] = "EUR";
@state() private _timeZone?: ConfigUpdateValues["time_zone"]; private _timeZone: ConfigUpdateValues["time_zone"] =
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
@state() private _language: ConfigUpdateValues["language"]; private _language: ConfigUpdateValues["language"] = getLocalLanguage();
@state() private _country?: ConfigUpdateValues["country"]; @state() private _country?: ConfigUpdateValues["country"];
@ -69,155 +61,28 @@ class OnboardingCoreConfig extends LitElement {
</div>`; </div>`;
} }
return html` return html`
${ ${this._error
this._error ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` : nothing}
: nothing
}
<p> <p>
${this.onboardingLocalize( ${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.intro_core_config" "ui.panel.page-onboarding.core-config.country_intro"
)} )}
</p> </p>
<div class="row"> <ha-country-picker
<ha-country-picker class="flex"
class="flex" .language=${this.hass.locale.language}
.language=${this.hass.locale.language} .label=${this.hass.localize(
.label=${ "ui.panel.config.core.section.core.core_config.country"
this.hass.localize( ) || "Country"}
"ui.panel.config.core.section.core.core_config.country" required
) || "Country" .disabled=${this._working}
} .value=${this._countryValue}
name="country" @value-changed=${this._handleCountryChanged}
required >
.disabled=${this._working} </ha-country-picker>
.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>
<div class="footer"> <div class="footer">
<mwc-button @click=${this._save} .disabled=${this._working}> <mwc-button @click=${this._save} .disabled=${this._working}>
@ -229,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) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("keyup", (ev) => { this.addEventListener("keyup", (ev) => {
@ -252,70 +103,49 @@ 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() { private get _countryValue() {
return this._country || ""; return this._country || "";
} }
private get _unitSystemValue() { private _handleCountryChanged(ev: ValueChangedEvent<string>) {
return this._unitSystem !== undefined ? this._unitSystem : "metric"; this._country = ev.detail.value;
}
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 async _locationChanged(ev) { private async _locationChanged(ev) {
this._location = ev.detail.value.location; this._location = ev.detail.value.location;
this._country = ev.detail.value.country; if (ev.detail.value.country) {
this._elevation = ev.detail.value.elevation; this._country = ev.detail.value.country;
this._currency = ev.detail.value.currency; }
this._language = ev.detail.value.language || getLocalLanguage(); if (ev.detail.value.elevation) {
this._timeZone = this._elevation = ev.detail.value.elevation;
ev.detail.value.timezone || }
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone; if (ev.detail.value.currency) {
this._unitSystem = ev.detail.value.unit_system; 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) { if (this._country) {
this._skipCore = true; this._skipCore = true;
this._save(ev); this._save(ev);
return; return;
} }
fireEvent(this, "onboarding-progress", { increase: 0.5 });
await this.updateComplete; await this.updateComplete;
setTimeout( setTimeout(
() => this.renderRoot.querySelector("ha-textfield")!.focus(), () => this.renderRoot.querySelector("ha-country-picker")!.focus(),
100 100
); );
} }
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as
| "metric"
| "us_customary";
}
private async _save(ev) { private async _save(ev) {
if (!this._location) { if (!this._location || !this._country) {
return; return;
} }
ev.preventDefault(); ev.preventDefault();
@ -327,12 +157,15 @@ class OnboardingCoreConfig extends LitElement {
), ),
latitude: this._location[0], latitude: this._location[0],
longitude: this._location[1], longitude: this._location[1],
elevation: Number(this._elevationValue), elevation: Number(this._elevation),
unit_system: this._unitSystemValue, unit_system:
time_zone: this._timeZoneValue || "UTC", this._unitSystem || ["US", "MM", "LR"].includes(this._country)
currency: this._currencyValue || "EUR", ? "us_customary"
country: this._countryValue, : "metric",
language: this._languageValue, time_zone: this._timeZone || "UTC",
currency: this._currency || countryCurrency[this._country] || "EUR",
country: this._country,
language: this._language,
}); });
const result = await onboardCoreConfigStep(this.hass); const result = await onboardCoreConfigStep(this.hass);
fireEvent(this, "onboarding-step", { fireEvent(this, "onboarding-step", {

View File

@ -1,7 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { genClientId } from "home-assistant-js-websocket"; import { genClientId } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
@ -16,6 +15,8 @@ import type { HaForm } from "../components/ha-form/ha-form";
import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types"; import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types";
import { onboardUserStep } from "../data/onboarding"; import { onboardUserStep } from "../data/onboarding";
import { ValueChangedEvent } from "../types"; import { ValueChangedEvent } from "../types";
import { onBoardingStyles } from "./styles";
import { debounce } from "../common/util/debounce";
const CREATE_USER_SCHEMA: HaFormSchema[] = [ const CREATE_USER_SCHEMA: HaFormSchema[] = [
{ {
@ -58,7 +59,7 @@ class OnboardingCreateUser extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` 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> <p>${this.localize("ui.panel.page-onboarding.user.intro")}</p>
${this._errorMsg ${this._errorMsg
@ -67,25 +68,27 @@ class OnboardingCreateUser extends LitElement {
<ha-form <ha-form
.computeLabel=${this._computeLabel(this.localize)} .computeLabel=${this._computeLabel(this.localize)}
.computeHelper=${this._computeHelper(this.localize)}
.data=${this._newUser} .data=${this._newUser}
.disabled=${this._loading} .disabled=${this._loading}
.error=${this._formError} .error=${this._formError}
.schema=${CREATE_USER_SCHEMA} .schema=${CREATE_USER_SCHEMA}
@value-changed=${this._handleValueChanged} @value-changed=${this._handleValueChanged}
></ha-form> ></ha-form>
<div class="footer">
<mwc-button <mwc-button
raised unelevated
@click=${this._submitForm} @click=${this._submitForm}
.disabled=${this._loading || .disabled=${this._loading ||
!this._newUser.name || !this._newUser.name ||
!this._newUser.username || !this._newUser.username ||
!this._newUser.password || !this._newUser.password ||
!this._newUser.password_confirm || !this._newUser.password_confirm ||
this._newUser.password !== this._newUser.password_confirm} this._newUser.password !== this._newUser.password_confirm}
> >
${this.localize("ui.panel.page-onboarding.user.create_account")} ${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button> </mwc-button>
</div>
`; `;
} }
@ -111,20 +114,48 @@ class OnboardingCreateUser extends LitElement {
localize(`ui.panel.page-onboarding.user.data.${schema.name}`); 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( private _handleValueChanged(
ev: ValueChangedEvent<HaFormDataContainer> ev: ValueChangedEvent<HaFormDataContainer>
): void { ): void {
const nameChanged = ev.detail.value.name !== this._newUser.name; 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; this._newUser = ev.detail.value;
if (nameChanged) { if (nameChanged) {
this._maybePopulateUsername(); 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._formError.password_confirm =
this._newUser.password_confirm &&
this._newUser.password !== this._newUser.password_confirm this._newUser.password !== this._newUser.password_confirm
? this.localize( ? this.localize(
"ui.panel.page-onboarding.user.error.password_not_match" "ui.panel.page-onboarding.user.error.password_not_match"
) )
: ""; : "";
if (old !== this._formError.password_confirm) {
this.requestUpdate("_formError");
}
} }
private _maybePopulateUsername(): void { private _maybePopulateUsername(): void {
@ -167,14 +198,7 @@ class OnboardingCreateUser extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return onBoardingStyles;
mwc-button {
margin: 32px 0 0;
text-align: center;
display: block;
text-align: right;
}
`;
} }
} }

View File

@ -21,6 +21,7 @@ import { scanUSBDevices } from "../data/usb";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./integration-badge"; import "./integration-badge";
import { onBoardingStyles } from "./styles";
const HIDDEN_DOMAINS = new Set([ const HIDDEN_DOMAINS = new Set([
"hassio", "hassio",
@ -131,11 +132,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
} }
return html` return html`
<h2> <h1>
${this.onboardingLocalize( ${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.header" "ui.panel.page-onboarding.integration.header"
)} )}
</h2> </h1>
<p> <p>
${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")} ${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")}
</p> </p>
@ -158,7 +159,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
: nothing} : nothing}
</div> </div>
<div class="footer"> <div class="footer">
<mwc-button @click=${this._finish}> <mwc-button unelevated @click=${this._finish}>
${this.onboardingLocalize( ${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish" "ui.panel.page-onboarding.integration.finish"
)} )}
@ -187,30 +188,23 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
h2 { onBoardingStyles,
text-align: center; css`
} .badges {
p { margin-top: 24px;
font-size: 14px; display: grid;
line-height: 20px; grid-template-columns: repeat(auto-fill, minmax(106px, 1fr));
} row-gap: 24px;
.badges { }
margin-top: 24px; .more {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); justify-content: center;
row-gap: 24px; align-items: center;
} height: 100%;
.more { }
display: flex; `,
justify-content: center; ];
align-items: center;
height: 100%;
}
.footer {
text-align: right;
}
`;
} }
} }

View File

@ -4,7 +4,7 @@ import { customElement } from "lit/decorators";
@customElement("onboarding-loading") @customElement("onboarding-loading")
class OnboardingLoading extends LitElement { class OnboardingLoading extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` <div class="loader"></div> `; return html`<div class="loader"></div>`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -1,5 +1,10 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCrosshairsGps, mdiMapMarker, mdiMapSearchOutline } from "@mdi/js"; import {
mdiCrosshairsGps,
mdiMagnify,
mdiMapMarker,
mdiMapSearchOutline,
} from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -30,6 +35,7 @@ import {
reverseGeocode, reverseGeocode,
searchPlaces, searchPlaces,
} from "../data/openstreetmap"; } from "../data/openstreetmap";
import { onBoardingStyles } from "./styles";
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147]; const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)"); const mql = matchMedia("(prefers-color-scheme: dark)");
@ -43,7 +49,7 @@ class OnboardingLocation extends LitElement {
@state() private _working = false; @state() private _working = false;
@state() private _location?: [number, number]; @state() private _location: [number, number] = AMSTERDAM;
@state() private _places?: OpenStreetMapPlace[] | null; @state() private _places?: OpenStreetMapPlace[] | null;
@ -87,6 +93,11 @@ class OnboardingLocation extends LitElement {
); );
return html` return html`
<h1>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_header"
)}
</h1>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing} : nothing}
@ -97,78 +108,85 @@ class OnboardingLocation extends LitElement {
)} )}
</p> </p>
<ha-textfield <div class="location-search">
label=${this.onboardingLocalize( <ha-textfield
"ui.panel.page-onboarding.core-config.address_label" label=${this.onboardingLocalize(
)} "ui.panel.page-onboarding.core-config.address_label"
.disabled=${this._working} )}
iconTrailing .disabled=${this._working}
@keyup=${this._addressSearch} icon
> iconTrailing
${this._working @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` ? html`
<ha-circular-progress <mwc-list activatable>
slot="trailingIcon" ${this._places?.length
active ? this._places.map((place) => {
size="small" const primary = [
></ha-circular-progress> 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` : nothing}
<ha-icon-button </div>
@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>
<ha-locations-editor <ha-locations-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
@ -184,11 +202,10 @@ class OnboardingLocation extends LitElement {
@marker-clicked=${this._markerClicked} @marker-clicked=${this._markerClicked}
></ha-locations-editor> ></ha-locations-editor>
<p class="attribution">${addressAttribution}</p>
<div class="footer"> <div class="footer">
<mwc-button <mwc-button @click=${this._save} unelevated .disabled=${this._working}>
@click=${this._save}
.disabled=${!this._location || this._working}
>
${this.onboardingLocalize( ${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.finish" "ui.panel.page-onboarding.core-config.finish"
)} )}
@ -301,7 +318,6 @@ class OnboardingLocation extends LitElement {
private async _searchAddress(address: string) { private async _searchAddress(address: string) {
this._working = true; this._working = true;
this._location = undefined;
this._highlightedMarker = undefined; this._highlightedMarker = undefined;
this._error = undefined; this._error = undefined;
this._places = null; this._places = null;
@ -464,74 +480,76 @@ class OnboardingLocation extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
p { onBoardingStyles,
font-size: 14px; css`
line-height: 20px; .location-search {
} margin-top: 32px;
ha-textfield { margin-bottom: 32px;
display: block; }
} ha-textfield {
ha-textfield > ha-icon-button { display: block;
position: absolute; }
top: 10px; ha-textfield > ha-icon-button {
right: 10px; position: absolute;
--mdc-icon-button-size: 36px; top: 10px;
--mdc-icon-size: 20px; right: 10px;
color: var(--secondary-text-color); --mdc-icon-button-size: 36px;
inset-inline-start: initial; --mdc-icon-size: 20px;
inset-inline-end: 10px; color: var(--secondary-text-color);
direction: var(--direction); inset-inline-start: initial;
} inset-inline-end: 10px;
ha-textfield > ha-circular-progress { direction: var(--direction);
position: relative; }
left: 12px; ha-textfield > ha-circular-progress {
} position: relative;
ha-locations-editor { left: 12px;
display: block; }
height: 300px; ha-locations-editor {
margin-top: 8px; display: block;
border-radius: var(--mdc-shape-small, 4px); height: 300px;
overflow: hidden; margin-top: 8px;
} border-radius: var(--mdc-shape-large, 16px);
mwc-list { overflow: hidden;
width: 100%; }
border: 1px solid var(--divider-color); mwc-list {
box-sizing: border-box; width: 100%;
border-top-width: 0; border: 1px solid var(--divider-color);
border-bottom-left-radius: var(--mdc-shape-small, 4px); box-sizing: border-box;
border-bottom-right-radius: var(--mdc-shape-small, 4px); border-top-width: 0;
--mdc-list-vertical-padding: 0; border-bottom-left-radius: var(--mdc-shape-small, 4px);
} border-bottom-right-radius: var(--mdc-shape-small, 4px);
ha-list-item { --mdc-list-vertical-padding: 0;
height: 72px; }
} ha-list-item {
.footer { height: 72px;
margin-top: 16px; }
text-align: right; .attribution {
} /* textfield helper style */
.attribution { margin: 0;
/* textfield helper style */ padding: 4px 16px 12px 16px;
margin: 0; color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
padding: 4px 16px 12px 16px; font-family: var(
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6)); --mdc-typography-caption-font-family,
font-family: var( var(--mdc-typography-font-family, Roboto, sans-serif)
--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);
font-size: var(--mdc-typography-caption-font-size, 0.75rem); letter-spacing: var(
font-weight: var(--mdc-typography-caption-font-weight, 400); --mdc-typography-caption-letter-spacing,
letter-spacing: var( 0.0333333333em
--mdc-typography-caption-letter-spacing, );
0.0333333333em text-decoration: var(
); --mdc-typography-caption-text-decoration,
text-decoration: var(--mdc-typography-caption-text-decoration, inherit); inherit
text-transform: var(--mdc-typography-caption-text-transform, inherit); );
} text-transform: var(--mdc-typography-caption-text-transform, inherit);
.attribution a { }
color: inherit; .attribution a {
} color: inherit;
`; }
`,
];
} }
} }

View File

@ -1,44 +1,34 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload"; import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import "../components/ha-ansi-to-html"; import "../components/ha-ansi-to-html";
import "../components/ha-card";
import { fetchInstallationType } from "../data/onboarding"; 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"; import "./onboarding-loading";
import { onBoardingStyles } from "./styles";
declare global {
interface HASSDomEvents {
restoring: undefined;
}
}
@customElement("onboarding-restore-backup") @customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) { class OnboardingRestoreBackup extends LitElement {
@property() public localize!: LocalizeFunc; @property() public localize!: LocalizeFunc;
@property() public language!: string; @property() public language!: string;
@property({ type: Boolean }) public restoring = false; @state() public _restoring = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return this.restoring return this._restoring
? html`<ha-card ? html`<h1>
.header=${this.localize( ${this.localize("ui.panel.page-onboarding.restore.in_progress")}
"ui.panel.page-onboarding.restore.in_progress" </h1>
)} <onboarding-loading></onboarding-loading>`
>
<onboarding-loading></onboarding-loading>
</ha-card>`
: html` : html`
<button class="link" @click=${this._uploadBackup}> <h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
${this.localize("ui.panel.page-onboarding.restore.description")} <ha-button unelevated @click=${this._uploadBackup}>
</button> ${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</ha-button>
`; `;
} }
@ -51,12 +41,11 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
makeDialogManager(this, this.shadowRoot!);
setInterval(() => this._checkRestoreStatus(), 1000); setInterval(() => this._checkRestoreStatus(), 1000);
} }
private async _checkRestoreStatus(): Promise<void> { private async _checkRestoreStatus(): Promise<void> {
if (this.restoring) { if (this._restoring) {
try { try {
await fetchInstallationType(); await fetchInstallationType();
} catch (err: any) { } catch (err: any) {
@ -72,32 +61,20 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
slug, slug,
onboarding: true, onboarding: true,
localize: this.localize, localize: this.localize,
onRestoring: () => {
this._restoring = true;
},
}); });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, onBoardingStyles,
css` css`
.logentry { :host {
text-align: center; display: flex;
} flex-direction: column;
ha-card { align-items: center;
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;
}
} }
`, `,
]; ];

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -1,5 +1,6 @@
import { tsParticles } from "tsparticles-engine"; import { tsParticles } from "tsparticles-engine";
import { loadLinksPreset } from "tsparticles-preset-links"; import { loadLinksPreset } from "tsparticles-preset-links";
import { DEFAULT_PRIMARY_COLOR } from "../resources/ha-style";
loadLinksPreset(tsParticles).then(() => { loadLinksPreset(tsParticles).then(() => {
tsParticles.load("particles", { tsParticles.load("particles", {
@ -22,16 +23,16 @@ loadLinksPreset(tsParticles).then(() => {
}, },
particles: { particles: {
color: { color: {
value: "#fff", value: DEFAULT_PRIMARY_COLOR,
animation: { },
enable: true, animation: {
speed: 50, enable: true,
sync: false, speed: 50,
}, sync: false,
}, },
links: { links: {
color: { color: {
value: "random", value: DEFAULT_PRIMARY_COLOR,
}, },
distance: 100, distance: 100,
enable: true, enable: true,

19
src/onboarding/styles.ts Normal file
View 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;
}
`;

View File

@ -5687,8 +5687,21 @@
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?", "intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
"next": "Next", "next": "Next",
"finish": "Finish", "finish": "Finish",
"language": "Language", "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": { "user": {
"header": "Create user",
"intro": "Let's get started by creating a user account.", "intro": "Let's get started by creating a user account.",
"required_field": "Required", "required_field": "Required",
"data": { "data": {
@ -5697,18 +5710,22 @@
"password": "Password", "password": "Password",
"password_confirm": "Confirm 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", "create_account": "Create account",
"error": { "error": {
"password_not_match": "Passwords don't match" "password_not_match": "Passwords don't match"
} }
}, },
"core-config": { "core-config": {
"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.", "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}).", "location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).",
"osm_privacy_policy": "Privacy policy", "osm_privacy_policy": "Privacy policy",
"title_location_detect": "Do you want us to detect your location?", "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_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": "Name of your Home Assistant installation",
"location_name_default": "Home", "location_name_default": "Home",
"address_label": "Search address", "address_label": "Search address",
@ -5716,12 +5733,13 @@
"finish": "Next" "finish": "Next"
}, },
"integration": { "integration": {
"header": "We already found compatible devices on your network!", "header": "We found compatible devices!",
"intro": "We have set some of them up for you. Some might need more configuration.", "intro": "These are found on your local network. Some are already added, others may need extra configuration.",
"more_integrations": "+{count} more", "more_integrations": "+{count} more",
"finish": "Finish" "finish": "Finish"
}, },
"analytics": { "analytics": {
"header": "Help us help you",
"finish": "Next", "finish": "Next",
"preferences": { "preferences": {
"base": { "base": {
@ -5746,20 +5764,8 @@
"intro": "[%key:ui::panel::config::analytics::intro%]" "intro": "[%key:ui::panel::config::analytics::intro%]"
}, },
"restore": { "restore": {
"description": "Alternatively you can restore from a previous backup.", "header": "Restore a backup",
"in_progress": "Restore in progress", "in_progress": "Restore in progress",
"show_log": "Show full log",
"hide_log": "Hide full log",
"full_backup": "[%key:supervisor::backup::full_backup%]",
"partial_backup": "[%key:supervisor::backup::partial_backup%]",
"name": "[%key:supervisor::backup::name%]",
"type": "[%key:supervisor::backup::type%]",
"select_type": "[%key:supervisor::backup::select_type%]",
"folders": "[%key:supervisor::backup::folders%]",
"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%]" "upload_backup": "[%key:supervisor::backup::upload_backup%]"
} }
}, },