Use nominatim from openstreetmap for location search in onboarding (#17287)
* Use nominatim from openstreetmap for location search in onboarding * Update text, add user agent * Handle errors better, add email address * remove detect text * Use `ui.common.search` * Update attribution location * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Update src/translations/en.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Remove unused style * Increase line-height * Apply suggestions --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com>pull/17337/head
parent
bc3295d851
commit
d56273ec25
|
@ -182,6 +182,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {
|
||||
import type {
|
||||
Circle,
|
||||
DivIcon,
|
||||
DragEndEvent,
|
||||
LatLng,
|
||||
LatLngExpression,
|
||||
Marker,
|
||||
MarkerOptions,
|
||||
} from "leaflet";
|
||||
|
@ -22,6 +23,8 @@ import type { HomeAssistant } from "../../types";
|
|||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
import { HaIcon } from "../ha-icon";
|
||||
import { HaSvgIcon } from "../ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
|
@ -40,6 +43,7 @@ export interface MarkerLocation {
|
|||
name?: string;
|
||||
id: string;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
radius_color?: string;
|
||||
location_editable?: boolean;
|
||||
radius_editable?: boolean;
|
||||
|
@ -81,11 +85,21 @@ export class HaLocationsEditor extends LitElement {
|
|||
);
|
||||
}
|
||||
|
||||
public fitMap(): void {
|
||||
this.map.fitMap();
|
||||
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||
this.map.fitMap(options);
|
||||
}
|
||||
|
||||
public async fitMarker(id: string): Promise<void> {
|
||||
public fitBounds(
|
||||
boundingbox: LatLngExpression[],
|
||||
options?: { zoom?: number; pad?: number }
|
||||
) {
|
||||
this.map.fitBounds(boundingbox, options);
|
||||
}
|
||||
|
||||
public async fitMarker(
|
||||
id: string,
|
||||
options?: { zoom?: number }
|
||||
): Promise<void> {
|
||||
if (!this.Leaflet) {
|
||||
await this._loadPromise;
|
||||
}
|
||||
|
@ -104,7 +118,10 @@ export class HaLocationsEditor extends LitElement {
|
|||
if (circle) {
|
||||
this.map.leafletMap.fitBounds(circle.getBounds());
|
||||
} else {
|
||||
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
|
||||
this.map.leafletMap.setView(
|
||||
marker.getLatLng(),
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,15 +216,21 @@ export class HaLocationsEditor extends LitElement {
|
|||
|
||||
this.locations.forEach((location: MarkerLocation) => {
|
||||
let icon: DivIcon | undefined;
|
||||
if (location.icon) {
|
||||
if (location.icon || location.iconPath) {
|
||||
// create icon
|
||||
const el = document.createElement("div");
|
||||
el.className = "named-icon";
|
||||
if (location.name) {
|
||||
if (location.name !== undefined) {
|
||||
el.innerText = location.name;
|
||||
}
|
||||
const iconEl = document.createElement("ha-icon");
|
||||
let iconEl: HaIcon | HaSvgIcon;
|
||||
if (location.icon) {
|
||||
iconEl = document.createElement("ha-icon");
|
||||
iconEl.setAttribute("icon", location.icon);
|
||||
} else {
|
||||
iconEl = document.createElement("ha-svg-icon");
|
||||
iconEl.setAttribute("path", location.iconPath!);
|
||||
}
|
||||
el.prepend(iconEl);
|
||||
|
||||
icon = this.Leaflet!.divIcon({
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
LatLngExpression,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
|
@ -162,7 +163,7 @@ export class HaMap extends ReactiveElement {
|
|||
this._loaded = true;
|
||||
}
|
||||
|
||||
public fitMap(): void {
|
||||
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
@ -173,7 +174,7 @@ export class HaMap extends ReactiveElement {
|
|||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
this.zoom
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -196,11 +197,22 @@ export class HaMap extends ReactiveElement {
|
|||
);
|
||||
});
|
||||
|
||||
if (!this.layers) {
|
||||
bounds = bounds.pad(0.5);
|
||||
bounds = bounds.pad(options?.pad ?? 0.5);
|
||||
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
}
|
||||
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
|
||||
public fitBounds(
|
||||
boundingbox: LatLngExpression[],
|
||||
options?: { zoom?: number; pad?: number }
|
||||
) {
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
options?.pad ?? 0.5
|
||||
);
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
}
|
||||
|
||||
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
// From http://country.io/currency.json
|
||||
|
||||
export const countryCurrency = {
|
||||
BD: "BDT",
|
||||
BE: "EUR",
|
||||
BF: "XOF",
|
||||
BG: "BGN",
|
||||
BA: "BAM",
|
||||
BB: "BBD",
|
||||
WF: "XPF",
|
||||
BL: "EUR",
|
||||
BM: "BMD",
|
||||
BN: "BND",
|
||||
BO: "BOB",
|
||||
BH: "BHD",
|
||||
BI: "BIF",
|
||||
BJ: "XOF",
|
||||
BT: "BTN",
|
||||
JM: "JMD",
|
||||
BV: "NOK",
|
||||
BW: "BWP",
|
||||
WS: "WST",
|
||||
BQ: "USD",
|
||||
BR: "BRL",
|
||||
BS: "BSD",
|
||||
JE: "GBP",
|
||||
BY: "BYN",
|
||||
BZ: "BZD",
|
||||
RU: "RUB",
|
||||
RW: "RWF",
|
||||
RS: "RSD",
|
||||
TL: "USD",
|
||||
RE: "EUR",
|
||||
TM: "TMT",
|
||||
TJ: "TJS",
|
||||
RO: "RON",
|
||||
TK: "NZD",
|
||||
GW: "XOF",
|
||||
GU: "USD",
|
||||
GT: "GTQ",
|
||||
GS: "GBP",
|
||||
GR: "EUR",
|
||||
GQ: "XAF",
|
||||
GP: "EUR",
|
||||
JP: "JPY",
|
||||
GY: "GYD",
|
||||
GG: "GBP",
|
||||
GF: "EUR",
|
||||
GE: "GEL",
|
||||
GD: "XCD",
|
||||
GB: "GBP",
|
||||
GA: "XAF",
|
||||
SV: "USD",
|
||||
GN: "GNF",
|
||||
GM: "GMD",
|
||||
GL: "DKK",
|
||||
GI: "GIP",
|
||||
GH: "GHS",
|
||||
OM: "OMR",
|
||||
TN: "TND",
|
||||
JO: "JOD",
|
||||
HR: "EUR",
|
||||
HT: "HTG",
|
||||
HU: "HUF",
|
||||
HK: "HKD",
|
||||
HN: "HNL",
|
||||
HM: "AUD",
|
||||
VE: "VEF",
|
||||
PR: "USD",
|
||||
PS: "ILS",
|
||||
PW: "USD",
|
||||
PT: "EUR",
|
||||
SJ: "NOK",
|
||||
PY: "PYG",
|
||||
IQ: "IQD",
|
||||
PA: "PAB",
|
||||
PF: "XPF",
|
||||
PG: "PGK",
|
||||
PE: "PEN",
|
||||
PK: "PKR",
|
||||
PH: "PHP",
|
||||
PN: "NZD",
|
||||
PL: "PLN",
|
||||
PM: "EUR",
|
||||
ZM: "ZMK",
|
||||
EH: "MAD",
|
||||
EE: "EUR",
|
||||
EG: "EGP",
|
||||
ZA: "ZAR",
|
||||
EC: "USD",
|
||||
IT: "EUR",
|
||||
VN: "VND",
|
||||
SB: "SBD",
|
||||
ET: "ETB",
|
||||
SO: "SOS",
|
||||
ZW: "ZWL",
|
||||
SA: "SAR",
|
||||
ES: "EUR",
|
||||
ER: "ERN",
|
||||
ME: "EUR",
|
||||
MD: "MDL",
|
||||
MG: "MGA",
|
||||
MF: "EUR",
|
||||
MA: "MAD",
|
||||
MC: "EUR",
|
||||
UZ: "UZS",
|
||||
MM: "MMK",
|
||||
ML: "XOF",
|
||||
MO: "MOP",
|
||||
MN: "MNT",
|
||||
MH: "USD",
|
||||
MK: "MKD",
|
||||
MU: "MUR",
|
||||
MT: "EUR",
|
||||
MW: "MWK",
|
||||
MV: "MVR",
|
||||
MQ: "EUR",
|
||||
MP: "USD",
|
||||
MS: "XCD",
|
||||
MR: "MRO",
|
||||
IM: "GBP",
|
||||
UG: "UGX",
|
||||
TZ: "TZS",
|
||||
MY: "MYR",
|
||||
MX: "MXN",
|
||||
IL: "ILS",
|
||||
FR: "EUR",
|
||||
IO: "USD",
|
||||
SH: "SHP",
|
||||
FI: "EUR",
|
||||
FJ: "FJD",
|
||||
FK: "FKP",
|
||||
FM: "USD",
|
||||
FO: "DKK",
|
||||
NI: "NIO",
|
||||
NL: "EUR",
|
||||
NO: "NOK",
|
||||
NA: "NAD",
|
||||
VU: "VUV",
|
||||
NC: "XPF",
|
||||
NE: "XOF",
|
||||
NF: "AUD",
|
||||
NG: "NGN",
|
||||
NZ: "NZD",
|
||||
NP: "NPR",
|
||||
NR: "AUD",
|
||||
NU: "NZD",
|
||||
CK: "NZD",
|
||||
XK: "EUR",
|
||||
CI: "XOF",
|
||||
CH: "CHF",
|
||||
CO: "COP",
|
||||
CN: "CNY",
|
||||
CM: "XAF",
|
||||
CL: "CLP",
|
||||
CC: "AUD",
|
||||
CA: "CAD",
|
||||
CG: "XAF",
|
||||
CF: "XAF",
|
||||
CD: "CDF",
|
||||
CZ: "CZK",
|
||||
CY: "EUR",
|
||||
CX: "AUD",
|
||||
CR: "CRC",
|
||||
CW: "ANG",
|
||||
CV: "CVE",
|
||||
CU: "CUP",
|
||||
SZ: "SZL",
|
||||
SY: "SYP",
|
||||
SX: "ANG",
|
||||
KG: "KGS",
|
||||
KE: "KES",
|
||||
SS: "SSP",
|
||||
SR: "SRD",
|
||||
KI: "AUD",
|
||||
KH: "KHR",
|
||||
KN: "XCD",
|
||||
KM: "KMF",
|
||||
ST: "STD",
|
||||
SK: "EUR",
|
||||
KR: "KRW",
|
||||
SI: "EUR",
|
||||
KP: "KPW",
|
||||
KW: "KWD",
|
||||
SN: "XOF",
|
||||
SM: "EUR",
|
||||
SL: "SLL",
|
||||
SC: "SCR",
|
||||
KZ: "KZT",
|
||||
KY: "KYD",
|
||||
SG: "SGD",
|
||||
SE: "SEK",
|
||||
SD: "SDG",
|
||||
DO: "DOP",
|
||||
DM: "XCD",
|
||||
DJ: "DJF",
|
||||
DK: "DKK",
|
||||
VG: "USD",
|
||||
DE: "EUR",
|
||||
YE: "YER",
|
||||
DZ: "DZD",
|
||||
US: "USD",
|
||||
UY: "UYU",
|
||||
YT: "EUR",
|
||||
UM: "USD",
|
||||
LB: "LBP",
|
||||
LC: "XCD",
|
||||
LA: "LAK",
|
||||
TV: "AUD",
|
||||
TW: "TWD",
|
||||
TT: "TTD",
|
||||
TR: "TRY",
|
||||
LK: "LKR",
|
||||
LI: "CHF",
|
||||
LV: "EUR",
|
||||
TO: "TOP",
|
||||
LT: "EUR",
|
||||
LU: "EUR",
|
||||
LR: "LRD",
|
||||
LS: "LSL",
|
||||
TH: "THB",
|
||||
TF: "EUR",
|
||||
TG: "XOF",
|
||||
TD: "XAF",
|
||||
TC: "USD",
|
||||
LY: "LYD",
|
||||
VA: "EUR",
|
||||
VC: "XCD",
|
||||
AE: "AED",
|
||||
AD: "EUR",
|
||||
AG: "XCD",
|
||||
AF: "AFN",
|
||||
AI: "XCD",
|
||||
VI: "USD",
|
||||
IS: "ISK",
|
||||
IR: "IRR",
|
||||
AM: "AMD",
|
||||
AL: "ALL",
|
||||
AO: "AOA",
|
||||
AQ: "",
|
||||
AS: "USD",
|
||||
AR: "ARS",
|
||||
AU: "AUD",
|
||||
AT: "EUR",
|
||||
AW: "AWG",
|
||||
IN: "INR",
|
||||
AX: "EUR",
|
||||
AZ: "AZN",
|
||||
IE: "EUR",
|
||||
ID: "IDR",
|
||||
UA: "UAH",
|
||||
QA: "QAR",
|
||||
MZ: "MZN",
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface OpenStreetMapPlace {
|
||||
place_id: number;
|
||||
licence: string;
|
||||
osm_type: string;
|
||||
osm_id: number;
|
||||
lat: string;
|
||||
lon: string;
|
||||
place_rank: number;
|
||||
category: string;
|
||||
type: string;
|
||||
importance: number;
|
||||
addresstype: string;
|
||||
name: string | null;
|
||||
display_name: string;
|
||||
address: {
|
||||
house_number?: string;
|
||||
road?: string;
|
||||
neighbourhood?: string;
|
||||
city?: string;
|
||||
municipality?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
postcode?: string;
|
||||
country_code: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
boundingbox: number[];
|
||||
}
|
||||
|
||||
export const searchPlaces = (
|
||||
address: string,
|
||||
hass: HomeAssistant,
|
||||
addressdetails?: boolean,
|
||||
limit?: number
|
||||
): Promise<OpenStreetMapPlace[]> =>
|
||||
fetch(
|
||||
`https://nominatim.openstreetmap.org/search.php?q=${address}&format=jsonv2${
|
||||
limit ? `&limit=${limit}` : ""
|
||||
}${addressdetails ? "&addressdetails=1" : ""}&accept-language=${
|
||||
hass.locale.language
|
||||
}&email=abuse@home-assistant.io`,
|
||||
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
throw new Error(res.statusText);
|
||||
});
|
||||
|
||||
export const reverseGeocode = (
|
||||
location: [number, number],
|
||||
hass: HomeAssistant,
|
||||
zoom?: number
|
||||
): Promise<OpenStreetMapPlace> =>
|
||||
fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse.php?lat=${location[0]}&lon=${
|
||||
location[1]
|
||||
}&accept-language=${hass.locale.language}&zoom=${
|
||||
zoom ?? 18
|
||||
}&format=jsonv2&email=abuse@home-assistant.io`,
|
||||
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
throw new Error(res.statusText);
|
||||
});
|
|
@ -11,13 +11,14 @@
|
|||
}
|
||||
body {
|
||||
height: auto;
|
||||
padding: 64px 0;
|
||||
}
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
padding: 20px 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
max-width: 432px;
|
||||
margin: 64px auto 0;
|
||||
margin: 0 auto;
|
||||
box-shadow: var(
|
||||
--ha-card-box-shadow,
|
||||
rgba(0, 0, 0, 0.25) 0px 54px 55px,
|
||||
|
|
|
@ -82,10 +82,13 @@ 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;
|
||||
|
@ -93,7 +96,6 @@ class OnboardingAnalytics extends LitElement {
|
|||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import {
|
|||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
|
@ -22,22 +22,13 @@ import "../components/ha-textfield";
|
|||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/ha-timezone-picker";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
MarkerLocation,
|
||||
} from "../components/map/ha-locations-editor";
|
||||
import {
|
||||
ConfigUpdateValues,
|
||||
detectCoreConfig,
|
||||
saveCoreConfig,
|
||||
} from "../data/core";
|
||||
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";
|
||||
|
||||
const amsterdam: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const locationMarkerId = "location";
|
||||
import "./onboarding-location";
|
||||
import "./onboarding-name";
|
||||
|
||||
@customElement("onboarding-core-config")
|
||||
class OnboardingCoreConfig extends LitElement {
|
||||
|
@ -57,19 +48,29 @@ class OnboardingCoreConfig extends LitElement {
|
|||
|
||||
@state() private _currency?: ConfigUpdateValues["currency"];
|
||||
|
||||
@state() private _timeZone? =
|
||||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
@state() private _timeZone?: ConfigUpdateValues["time_zone"];
|
||||
|
||||
@state() private _language: ConfigUpdateValues["language"] =
|
||||
getLocalLanguage();
|
||||
@state() private _language: ConfigUpdateValues["language"];
|
||||
|
||||
@state() private _country?: ConfigUpdateValues["country"];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
||||
|
||||
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}
|
||||
.onboardingLocalize=${this.onboardingLocalize}
|
||||
@value-changed=${this._locationChanged}
|
||||
></onboarding-location>`;
|
||||
}
|
||||
return html`
|
||||
${
|
||||
this._error
|
||||
|
@ -79,54 +80,10 @@ class OnboardingCoreConfig extends LitElement {
|
|||
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro",
|
||||
"name",
|
||||
this.hass.user!.name
|
||||
"ui.panel.page-onboarding.core-config.intro_core_config"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-textfield
|
||||
.label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name"
|
||||
)}
|
||||
name="name"
|
||||
.disabled=${this._working}
|
||||
.value=${this._nameValue}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
|
||||
<div class="middle-text">
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_location"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_location_detect"
|
||||
)}
|
||||
</div>
|
||||
<mwc-button @click=${this._detect}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.button_detect"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(this._locationValue)}
|
||||
zoom="14"
|
||||
.darkMode=${mql.matches}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<ha-country-picker
|
||||
class="flex"
|
||||
|
@ -275,31 +232,29 @@ class OnboardingCoreConfig extends LitElement {
|
|||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
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);
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this.addEventListener("keyup", (ev) => {
|
||||
if (this._location && 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 get _locationValue() {
|
||||
return this._location || amsterdam;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined ? this._elevation : 0;
|
||||
}
|
||||
|
@ -324,17 +279,6 @@ class OnboardingCoreConfig extends LitElement {
|
|||
return this._currency !== undefined ? this._currency : "";
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(location: [number, number]): MarkerLocation[] => [
|
||||
{
|
||||
id: locationMarkerId,
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
this[`_${target.getAttribute("name")}`] = ev.detail.value;
|
||||
|
@ -345,8 +289,25 @@ class OnboardingCoreConfig extends LitElement {
|
|||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
private _nameChanged(ev: CustomEvent) {
|
||||
this._name = 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;
|
||||
await this.updateComplete;
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
|
@ -355,55 +316,17 @@ class OnboardingCoreConfig extends LitElement {
|
|||
| "us_customary";
|
||||
}
|
||||
|
||||
private async _detect() {
|
||||
this._working = true;
|
||||
try {
|
||||
const values = await detectCoreConfig(this.hass);
|
||||
|
||||
if (values.latitude && values.longitude) {
|
||||
this.map.addEventListener(
|
||||
"markers-updated",
|
||||
() => {
|
||||
this.map.fitMarker(locationMarkerId);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
this._location = [Number(values.latitude), Number(values.longitude)];
|
||||
}
|
||||
if (values.elevation) {
|
||||
this._elevation = String(values.elevation);
|
||||
}
|
||||
if (values.unit_system) {
|
||||
this._unitSystem = values.unit_system;
|
||||
}
|
||||
if (values.time_zone) {
|
||||
this._timeZone = values.time_zone;
|
||||
}
|
||||
if (values.currency) {
|
||||
this._currency = values.currency;
|
||||
}
|
||||
if (values.country) {
|
||||
this._country = values.country;
|
||||
}
|
||||
this._language = getLocalLanguage();
|
||||
} catch (err: any) {
|
||||
this._error = `Failed to detect location information: ${err.message}`;
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
if (!this._location) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._working = true;
|
||||
try {
|
||||
const location = this._locationValue;
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._nameValue,
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
location_name: this._name,
|
||||
latitude: this._location[0],
|
||||
longitude: this._location[1],
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue || "UTC",
|
||||
|
@ -436,12 +359,13 @@ class OnboardingCoreConfig extends LitElement {
|
|||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
display: block;
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
ha-locations-editor {
|
||||
height: 200px;
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flex {
|
||||
|
|
|
@ -211,6 +211,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
|||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.badges {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
|
|
|
@ -0,0 +1,542 @@
|
|||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCrosshairsGps, mdiMapMarker, mdiMapSearchOutline } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-textfield";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
MarkerLocation,
|
||||
} from "../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, detectCoreConfig } from "../data/core";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
OpenStreetMapPlace,
|
||||
reverseGeocode,
|
||||
searchPlaces,
|
||||
} from "../data/openstreetmap";
|
||||
|
||||
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const LOCATION_MARKER_ID = "location";
|
||||
|
||||
@customElement("onboarding-location")
|
||||
class OnboardingLocation extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public onboardingLocalize!: LocalizeFunc;
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _places?: OpenStreetMapPlace[] | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _search = false;
|
||||
|
||||
@state() private _highlightedMarker?: number;
|
||||
|
||||
private _elevation?: string;
|
||||
|
||||
private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
private _currency?: ConfigUpdateValues["currency"];
|
||||
|
||||
private _timeZone?: ConfigUpdateValues["time_zone"];
|
||||
|
||||
private _country?: ConfigUpdateValues["country"];
|
||||
|
||||
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const addressAttribution = this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_address",
|
||||
{
|
||||
openstreetmap: html`<a
|
||||
href="https://www.openstreetmap.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>OpenStreetMap</a
|
||||
>`,
|
||||
osm_privacy_policy: html`<a
|
||||
href="https://wiki.osmfoundation.org/wiki/Privacy_Policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.osm_privacy_policy"
|
||||
)}</a
|
||||
>`,
|
||||
}
|
||||
);
|
||||
|
||||
return html`
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_location"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-textfield
|
||||
label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.address_label"
|
||||
)}
|
||||
.disabled=${this._working}
|
||||
iconTrailing
|
||||
@keyup=${this._addressSearch}
|
||||
>
|
||||
${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`
|
||||
<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
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocations(
|
||||
this._location,
|
||||
this._places,
|
||||
this._highlightedMarker
|
||||
)}
|
||||
zoom="14"
|
||||
.darkMode=${mql.matches}
|
||||
.disabled=${this._working}
|
||||
@location-updated=${this._locationChanged}
|
||||
@marker-clicked=${this._markerClicked}
|
||||
></ha-locations-editor>
|
||||
|
||||
<div class="footer">
|
||||
<mwc-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._location || this._working}
|
||||
>
|
||||
${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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
if (changedProps.has("_highlightedMarker") && this._highlightedMarker) {
|
||||
const place = this._places?.find(
|
||||
(plc) => plc.place_id === this._highlightedMarker
|
||||
);
|
||||
if (place?.boundingbox?.length === 4) {
|
||||
this.map.fitBounds(
|
||||
[
|
||||
[place.boundingbox[0], place.boundingbox[2]],
|
||||
[place.boundingbox[1], place.boundingbox[3]],
|
||||
],
|
||||
{ zoom: 16, pad: 0 }
|
||||
);
|
||||
} else {
|
||||
this.map.fitMarker(String(this._highlightedMarker), { zoom: 16 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _markerLocations = memoizeOne(
|
||||
(
|
||||
location?: [number, number],
|
||||
places?: OpenStreetMapPlace[] | null,
|
||||
highlightedMarker?: number
|
||||
): MarkerLocation[] => {
|
||||
if (!places) {
|
||||
return [
|
||||
{
|
||||
id: LOCATION_MARKER_ID,
|
||||
latitude: (location || AMSTERDAM)[0],
|
||||
longitude: (location || AMSTERDAM)[1],
|
||||
location_editable: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return places?.length
|
||||
? places.map((place) => ({
|
||||
id: String(place.place_id),
|
||||
iconPath:
|
||||
place.place_id === highlightedMarker ? undefined : mdiMapMarker,
|
||||
latitude:
|
||||
location && place.place_id === highlightedMarker
|
||||
? location[0]
|
||||
: Number(place.lat),
|
||||
longitude:
|
||||
location && place.place_id === highlightedMarker
|
||||
? location[1]
|
||||
: Number(place.lon),
|
||||
location_editable: place.place_id === highlightedMarker,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
);
|
||||
|
||||
private _locationChanged(ev) {
|
||||
this._location = ev.detail.location;
|
||||
if (ev.detail.id !== LOCATION_MARKER_ID) {
|
||||
this._reverseGeocode();
|
||||
}
|
||||
}
|
||||
|
||||
private _markerClicked(ev) {
|
||||
if (ev.detail.id === LOCATION_MARKER_ID) {
|
||||
return;
|
||||
}
|
||||
this._highlightedMarker = ev.detail.id;
|
||||
const place = this._places!.find((plc) => plc.place_id === ev.detail.id)!;
|
||||
this._location = [Number(place.lat), Number(place.lon)];
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
|
||||
private _itemClicked(ev) {
|
||||
this._highlightedMarker = ev.currentTarget.placeId;
|
||||
const place = this._places!.find(
|
||||
(plc) => plc.place_id === ev.currentTarget.placeId
|
||||
)!;
|
||||
this._location = [Number(place.lat), Number(place.lon)];
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
|
||||
private async _addressSearch(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
this._search = (ev.currentTarget as HaTextField).value.length > 0;
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
this._searchAddress((ev.currentTarget as HaTextField).value);
|
||||
}
|
||||
|
||||
private async _searchAddress(address: string) {
|
||||
this._working = true;
|
||||
this._location = undefined;
|
||||
this._highlightedMarker = undefined;
|
||||
this._error = undefined;
|
||||
this._places = null;
|
||||
this.map.addEventListener(
|
||||
"markers-updated",
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
if ((this._places?.length || 0) > 2) {
|
||||
this.map.fitMap({ pad: 0.5 });
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
try {
|
||||
this._places = await searchPlaces(address, this.hass, true, 3);
|
||||
if (this._places?.length === 1) {
|
||||
this._highlightedMarker = this._places[0].place_id;
|
||||
this._location = [
|
||||
Number(this._places[0].lat),
|
||||
Number(this._places[0].lon),
|
||||
];
|
||||
this._country = this._places[0].address.country_code.toUpperCase();
|
||||
}
|
||||
} catch (e: any) {
|
||||
this._places = undefined;
|
||||
this._error = e.message;
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _reverseGeocode() {
|
||||
if (!this._location) {
|
||||
return;
|
||||
}
|
||||
this._places = null;
|
||||
const reverse = await reverseGeocode(this._location, this.hass);
|
||||
this._country = reverse.address.country_code.toUpperCase();
|
||||
this._places = [reverse];
|
||||
this._highlightedMarker = reverse.place_id;
|
||||
}
|
||||
|
||||
private async _handleButtonClick(ev) {
|
||||
if (this._search) {
|
||||
this._searchAddress(ev.target.parentElement.value);
|
||||
return;
|
||||
}
|
||||
this._detectLocation();
|
||||
}
|
||||
|
||||
private _detectLocation() {
|
||||
if (window.isSecureContext && navigator.geolocation) {
|
||||
this._working = true;
|
||||
const options = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0,
|
||||
};
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (result) => {
|
||||
this.map.addEventListener(
|
||||
"markers-updated",
|
||||
() => {
|
||||
this.map.fitMarker(LOCATION_MARKER_ID);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
this._location = [result.coords.latitude, result.coords.longitude];
|
||||
if (result.coords.altitude) {
|
||||
this._elevation = String(result.coords.altitude);
|
||||
}
|
||||
try {
|
||||
await this._reverseGeocode();
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// GPS is not available, get location based on IP
|
||||
this._working = false;
|
||||
this._whoAmI();
|
||||
},
|
||||
options
|
||||
);
|
||||
} else {
|
||||
this._whoAmI();
|
||||
}
|
||||
}
|
||||
|
||||
private async _whoAmI() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.title_location_detect"
|
||||
),
|
||||
text: this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro_location_detect"
|
||||
),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
this._working = true;
|
||||
try {
|
||||
const values = await detectCoreConfig(this.hass);
|
||||
|
||||
if (values.latitude && values.longitude) {
|
||||
this.map.addEventListener(
|
||||
"markers-updated",
|
||||
() => {
|
||||
this.map.fitMarker(LOCATION_MARKER_ID);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
this._location = [Number(values.latitude), Number(values.longitude)];
|
||||
}
|
||||
if (values.elevation) {
|
||||
this._elevation = String(values.elevation);
|
||||
}
|
||||
if (values.unit_system) {
|
||||
this._unitSystem = values.unit_system;
|
||||
}
|
||||
if (values.time_zone) {
|
||||
this._timeZone = values.time_zone;
|
||||
}
|
||||
if (values.currency) {
|
||||
this._currency = values.currency;
|
||||
}
|
||||
if (values.country) {
|
||||
this._country = values.country;
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = `Failed to detect location information: ${err.message}`;
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
if (!this._location) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
location: this._location!,
|
||||
country: this._country,
|
||||
elevation: this._elevation,
|
||||
unit_system: this._unitSystem,
|
||||
time_zone: this._timeZone,
|
||||
currency: this._currency,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-location": OnboardingLocation;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -5589,10 +5589,16 @@
|
|||
},
|
||||
"core-config": {
|
||||
"intro": "Hello {name}, welcome to Home Assistant. How would you like to name your home?",
|
||||
"intro_location": "We would like to know where you live. This information will help with displaying information and setting up sun-based automations. This data is never shared outside of your network.",
|
||||
"intro_location_detect": "We can help you fill in this information by making a one-time request to an external service.",
|
||||
"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_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.",
|
||||
"location_name": "Name of your Home Assistant installation",
|
||||
"location_name_default": "Home",
|
||||
"address_label": "Search address",
|
||||
"button_detect": "Detect",
|
||||
"finish": "Next"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue