`;
}
- private _handleInputKeyDown(ev: KeyboardEvent) {
- // Handle pressing enter.
- if (ev.key === "Enter") {
- this._handleSave();
+ private _handleDialogClose() {
+ switch (this._dialog.returnValue) {
+ case "save":
+ fireEvent(this, "save", {
+ instanceIndex: this.instanceIndex,
+ instanceName: this._instanceName,
+ instanceUrl: this._instanceUrl,
+ });
+ return;
+ case "delete":
+ fireEvent(this, "delete", { instanceIndex: this.instanceIndex });
+ return;
+ case "cancel":
+ if (!this.instance.url) {
+ // As a special case, if cancelled and url === "" then delete it.
+ // This happens when adding a new instance.
+ fireEvent(this, "delete", { instanceIndex: this.instanceIndex });
+ }
+ return;
+ default:
+ return;
}
}
private _handleSave() {
- const inputEl = this._textfield!;
- let value = inputEl.value || "";
- this._error = undefined;
+ const instanceUrlField = this.renderRoot.querySelector(
+ "md-filled-text-field#dialog-instance-url",
+ ) as MdFilledTextField;
- if (value === "") {
- value = DEFAULT_HASS_URL;
- }
+ let value = instanceUrlField?.value || "";
- if (value.indexOf("://") === -1) {
- this._textfield.setCustomValidity(
+ if (value == "" || value.indexOf("://") === -1) {
+ instanceUrlField.setCustomValidity(
"Please enter your full URL, including the protocol part (https://).",
);
- this._textfield.reportValidity();
+ instanceUrlField.reportValidity();
return;
}
let urlObj: URL;
try {
urlObj = new URL(value);
- } catch (err) {
- this._textfield.setCustomValidity("Invalid URL");
- this._textfield.reportValidity();
+ } catch (err: any) {
+ instanceUrlField.setCustomValidity("Invalid URL: " + err.toString());
+ instanceUrlField.reportValidity();
return;
}
- const url = `${urlObj.protocol}//${urlObj.host}`;
- try {
- window.localStorage.setItem(HASS_URL, url);
- } catch (err) {
- this._error = "Failed to store your URL!";
- return;
- }
- fireEvent(this, "value-changed", { value: url });
- }
+ this._instanceName =
+ (
+ this.renderRoot.querySelector(
+ "md-filled-text-field#dialog-instance-name",
+ ) as MdFilledTextField
+ )?.value || DEFAULT_INSTANCE_NAME;
+ this._instanceUrl = `${urlObj.protocol}//${urlObj.host}`;
- static get styles(): CSSResult {
- return css`
- :host {
- display: block;
- }
- div {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .error {
- color: #db4437;
- font-weight: bold;
- }
- md-filled-text-field {
- flex-grow: 1;
- margin-right: 8px;
- }
- `;
+ this._dialog.close("save");
}
}
diff --git a/src/const.ts b/src/const.ts
index 54e25e99..1b4feea0 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -1,5 +1,7 @@
-export const HASS_URL = "hassUrl";
-export const DEFAULT_HASS_URL = "http://homeassistant.local:8123";
+export const HASS_URL = "hassUrl"; // Deprecated.
+export const DEFAULT_INSTANCE_NAME = "home assistant instance";
+export const DEFAULT_INSTANCE_URL = "http://homeassistant.local:8123";
+export const DEFAULT_HASS_URL = DEFAULT_INSTANCE_URL; // Deprecated: use DEFAULT_INSTANCE_URL instead;
export const MOBILE_URL = "homeassistant://navigate";
export type ParamType = "url" | "string" | "string?" | "url?";
diff --git a/src/data/instance_info.ts b/src/data/instance_info.ts
index 29a44afe..22fbb122 100644
--- a/src/data/instance_info.ts
+++ b/src/data/instance_info.ts
@@ -1,9 +1,44 @@
import { HASS_URL, MOBILE_URL } from "../const";
import { isMobile } from "./is_mobile";
-export const getInstanceUrl = (): string | null => {
+const LOCAL_STORAGE_KEY = "instances";
+
+export interface Instance {
+ url: string;
+ name?: string;
+}
+
+export const getInstances = (): Instance[] | null => {
if (isMobile) {
- return MOBILE_URL;
+ return [{ url: MOBILE_URL }];
+ }
+
+ // Perform: One-time migration of legacy single URLs.
+ if (localStorage.getItem(HASS_URL)) {
+ localStorage.setItem(
+ LOCAL_STORAGE_KEY,
+ JSON.stringify([{ url: localStorage.getItem(HASS_URL) }]),
+ );
+ localStorage.removeItem(HASS_URL);
+ }
+
+ const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
+ if (!raw) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ console.error("failed to parse stored urls", e);
+ return [];
+ }
+};
+
+export const saveInstances = (instances: Instance[] | null) => {
+ if (!instances) {
+ localStorage.removeItem(LOCAL_STORAGE_KEY);
+ return;
}
- return localStorage.getItem(HASS_URL);
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(instances));
};
diff --git a/src/entrypoints/my-change-url.ts b/src/entrypoints/my-change-url.ts
index 4fd3e0ff..e205f4a2 100644
--- a/src/entrypoints/my-change-url.ts
+++ b/src/entrypoints/my-change-url.ts
@@ -1,7 +1,7 @@
import { html, LitElement, TemplateResult, PropertyValues } from "lit";
import { state, query, customElement } from "lit/decorators.js";
import "../components/my-url-input";
-import { getInstanceUrl } from "../data/instance_info";
+import { getInstances } from "../data/instance_info";
import { extractSearchParamsObject } from "../util/search-params";
import { MyUrlInputMain } from "../components/my-url-input";
import { isMobile } from "../data/is_mobile";
@@ -10,7 +10,7 @@ const changeRequestedFromRedirect = extractSearchParamsObject().redirect;
@customElement("my-change-url")
class MyChangeUrl extends LitElement {
- @state() private _instanceUrl = getInstanceUrl();
+ @state() private _instances = getInstances();
@state() private _error?: string;
@@ -44,7 +44,7 @@ class MyChangeUrl extends LitElement {
}
protected shouldUpdate() {
- return this._instanceUrl !== undefined;
+ return this._instances !== undefined;
}
protected render(): TemplateResult {
@@ -55,7 +55,7 @@ class MyChangeUrl extends LitElement {
}
return html`
- ${changeRequestedFromRedirect && !this._instanceUrl
+ ${changeRequestedFromRedirect && !this._instances
? html`
You are seeing this page because you have been linked to a page in
@@ -66,7 +66,7 @@ class MyChangeUrl extends LitElement {
`
: ""}
- ${this._instanceUrl
+ ${this._instances
? html`
Configure My Home Assistant by entering the URL of your Home
Assistant instance.
@@ -74,13 +74,13 @@ class MyChangeUrl extends LitElement {
: ""}
${this._error ? html`
${this._error}
` : ""}
-
Note: This URL is only stored in your browser.
+
Note: The URLs are only stored in your browser.
`;
}
diff --git a/src/entrypoints/my-index.ts b/src/entrypoints/my-index.ts
index aecc99d3..3deaeeed 100644
--- a/src/entrypoints/my-index.ts
+++ b/src/entrypoints/my-index.ts
@@ -1,92 +1,133 @@
-import { html, LitElement, TemplateResult, PropertyValues } from "lit";
+import "@material/web/button/outlined-button";
+
+import { html, LitElement, TemplateResult } from "lit";
import { customElement, state, query } from "lit/decorators.js";
import "../components/my-url-input";
import "../components/my-instance-info";
-import { getInstanceUrl } from "../data/instance_info";
-import { MyUrlInputMain } from "../components/my-url-input";
+import { getInstances, Instance, saveInstances } from "../data/instance_info";
+
@customElement("my-index")
class MyIndex extends LitElement {
- @state() private _updatingUrl = false;
-
- @state() private _instanceUrl!: string | null;
-
- @state() private _error?: string;
-
- @query("my-url-input") private _urlInput?: MyUrlInputMain;
-
- createRenderRoot() {
- while (this.lastChild) {
- this.removeChild(this.lastChild);
- }
- return this;
- }
+ @state() private _instances!: Instance[] | null;
public connectedCallback() {
super.connectedCallback();
- this._instanceUrl = getInstanceUrl();
- if (!this._updatingUrl && !this._instanceUrl) {
- this._updatingUrl = true;
- }
- }
-
- protected updated(changedProps: PropertyValues) {
- if (changedProps.has("_updatingUrl") && this._updatingUrl) {
- this.updateComplete.then(() => this._urlInput?.focus());
- }
+ this._instances = getInstances();
}
protected shouldUpdate() {
- return this._instanceUrl !== undefined;
+ return this._instances !== undefined;
}
- protected render(): TemplateResult {
- if (this._updatingUrl) {
- return html`
-
- ${!this._instanceUrl
- ? html`
-
- Configure My Home Assistant by entering the URL of your Home
- Assistant instance.
-
- `
- : ""}
-
-
-
- ${this._error ? html`
${this._error}
` : ""}
-
-
Note: This URL is only stored in your browser.
-
- `;
- }
-
+ /*protected render(): TemplateResult {
return html`
`;
+ }*/
+
+ protected render(): TemplateResult {
+ console.log("this.instances", this._instances);
+ return html`
+
+
+ ${this._instances?.map(
+ (instance, index) =>
+ html`
` /*
+
+
+ ${instance.name || "HOME ASSISTANT INSTANCE"}
+
+
+
+ `,*/,
+ )}
+
+ Add Instance
+
+
+ `;
}
- private _handleEdit() {
- this._updatingUrl = true;
+ _handleNewInstanceClick() {
+ if (!this._instances) {
+ this._instances = [
+ {
+ url: "",
+ },
+ ];
+ } else {
+ this._instances = [...this._instances, { url: "" }];
+ }
}
- private _handleUrlChanged(ev: CustomEvent) {
- const instanceUrl = ev.detail.value;
+ _handleSave(ev: CustomEvent) {
+ const instanceIndex = ev.detail.instanceIndex;
+ if (instanceIndex === undefined) {
+ return;
+ }
- if (!instanceUrl) {
- this._error = "You need to configure a URL to use My Home Assistant.";
+ const instance: Instance = {
+ name: ev.detail.instanceName,
+ url: ev.detail.instanceUrl,
+ };
+ // In-place operations (such as .pop) don't trigger re-renders so use a temporary array, mutate it, and then set this._instances.
+ let tmp = [...(this._instances || [])];
+ if (!this._instances) {
+ tmp = [instance];
+ return;
+ } else if (this._instances.length >= instanceIndex) {
+ tmp[instanceIndex] = instance;
+ } else {
+ tmp.push(instance);
+ }
+ this._instances = tmp;
+ saveInstances(this._instances);
+ }
+ _handleDelete(ev: CustomEvent) {
+ const instanceIndex = ev.detail.instanceIndex;
+ if (instanceIndex === undefined) {
return;
}
- this._error = undefined;
- this._updatingUrl = false;
- this._instanceUrl = instanceUrl;
+ // .splice changes the array in place which does not trigger a re-render.
+ // By using a temporary copy of the array, splicing it, and then setting this._instances to that spliced value
+ // a re-render is triggered.
+ const tmp = [...(this._instances || [])];
+ tmp.splice(instanceIndex, 1) || null;
+ this._instances = tmp;
+ saveInstances(this._instances);
}
}
diff --git a/src/entrypoints/my-redirect.ts b/src/entrypoints/my-redirect.ts
index 824c4783..79dea281 100644
--- a/src/entrypoints/my-redirect.ts
+++ b/src/entrypoints/my-redirect.ts
@@ -4,7 +4,7 @@ import {
createSearchParam,
extractSearchParamsObject,
} from "../util/search-params";
-import { getInstanceUrl } from "../data/instance_info";
+import { getInstances } from "../data/instance_info";
import { Redirect } from "../const";
import { svgPencil } from "../components/svg-pencil";
import { isMobile } from "../data/is_mobile";
@@ -40,7 +40,7 @@ const createRedirectParams = (): string => {
let changingInstance = false;
const render = (showTroubleshooting: boolean) => {
- const instanceUrl = getInstanceUrl();
+ const instances = getInstances();
let params;
try {
@@ -59,7 +59,7 @@ const render = (showTroubleshooting: boolean) => {
window.redirect.redirect + "/" + params,
)}`;
- if (instanceUrl === null) {
+ if (instances === null) {
changingInstance = true;
document.location.assign(changeUrl);
return;
@@ -67,8 +67,8 @@ const render = (showTroubleshooting: boolean) => {
const redirectUrl =
window.redirect.redirect === "oauth"
- ? `${instanceUrl}/auth/external/callback${params}`
- : `${instanceUrl}/_my_redirect/${window.redirect.redirect}${params}`;
+ ? `${instances[0]}/auth/external/callback${params}`
+ : `${instances[0]}/_my_redirect/${window.redirect.redirect}${params}`;
const openLink = document.querySelector(".open-link") as HTMLElement;
openLink.outerHTML = `