diff --git a/src/common/decorators/local-storage.ts b/src/common/decorators/storage.ts similarity index 64% rename from src/common/decorators/local-storage.ts rename to src/common/decorators/storage.ts index 752b95933e..a35f0b414a 100644 --- a/src/common/decorators/local-storage.ts +++ b/src/common/decorators/storage.ts @@ -1,13 +1,15 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { PropertyDeclaration, ReactiveElement } from "lit"; +import { ReactiveElement } from "lit"; +import { InternalPropertyDeclaration } from "lit/decorators"; import type { ClassElement } from "../../types"; type Callback = (oldValue: any, newValue: any) => void; -class Storage { - constructor(subscribe = true, storage = window.localStorage) { +class StorageClass { + constructor(storage = window.localStorage) { this.storage = storage; - if (!subscribe) { + if (storage !== window.localStorage) { + // storage events only work for localStorage return; } window.addEventListener("storage", (ev: StorageEvent) => { @@ -77,6 +79,7 @@ class Storage { } public setValue(storageKey: string, value: any): any { + const oldValue = this._storage[storageKey]; this._storage[storageKey] = value; try { if (value === undefined) { @@ -86,49 +89,68 @@ class Storage { } } catch (err: any) { // Safari in private mode doesn't allow localstorage + } finally { + if (this._listeners[storageKey]) { + this._listeners[storageKey].forEach((listener) => + listener(oldValue, value) + ); + } } } } -const subscribeStorage = new Storage(); +const storages: Record = {}; -export const LocalStorage = - ( - storageKey?: string, - property?: boolean, - subscribe = true, - storageType?: globalThis.Storage, - propertyOptions?: PropertyDeclaration - ): any => +export const storage = + (options: { + key?: string; + storage?: "localStorage" | "sessionStorage"; + subscribe?: boolean; + state?: boolean; + stateOptions?: InternalPropertyDeclaration; + }): any => (clsElement: ClassElement) => { - const storage = - subscribe && !storageType - ? subscribeStorage - : new Storage(subscribe, storageType); + const storageName = options.storage || "localStorage"; + + let storageInstance: StorageClass; + if (storageName && storageName in storages) { + storageInstance = storages[storageName]; + } else { + storageInstance = new StorageClass(window[storageName]); + storages[storageName] = storageInstance; + } const key = String(clsElement.key); - storageKey = storageKey || String(clsElement.key); + const storageKey = options.key || String(clsElement.key); const initVal = clsElement.initializer ? clsElement.initializer() : undefined; - storage.addFromStorage(storageKey); + storageInstance.addFromStorage(storageKey); - const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc => - storage.subscribeChanges(storageKey!, (oldValue) => { - el.requestUpdate(clsElement.key, oldValue); - }); + const subscribeChanges = + options.subscribe !== false + ? (el: ReactiveElement): UnsubscribeFunc => + storageInstance.subscribeChanges( + storageKey!, + (oldValue, _newValue) => { + el.requestUpdate(clsElement.key, oldValue); + } + ) + : undefined; const getValue = (): any => - storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal; + storageInstance.hasKey(storageKey!) + ? storageInstance.getValue(storageKey!) + : initVal; const setValue = (el: ReactiveElement, value: any) => { let oldValue: unknown | undefined; - if (property) { + if (options.state) { oldValue = getValue(); } - storage.setValue(storageKey!, value); - if (property) { + storageInstance.setValue(storageKey!, value); + if (options.state) { el.requestUpdate(clsElement.key, oldValue); } }; @@ -148,22 +170,23 @@ export const LocalStorage = configurable: true, }, finisher(cls: typeof ReactiveElement) { - if (property && subscribe) { + if (options.state && options.subscribe) { const connectedCallback = cls.prototype.connectedCallback; const disconnectedCallback = cls.prototype.disconnectedCallback; cls.prototype.connectedCallback = function () { connectedCallback.call(this); - this[`__unbsubLocalStorage${key}`] = subscribeChanges(this); + this[`__unbsubLocalStorage${key}`] = subscribeChanges?.(this); }; cls.prototype.disconnectedCallback = function () { disconnectedCallback.call(this); - this[`__unbsubLocalStorage${key}`](); + this[`__unbsubLocalStorage${key}`]?.(); + this[`__unbsubLocalStorage${key}`] = undefined; }; } - if (property) { + if (options.state) { cls.createProperty(clsElement.key, { noAccessor: true, - ...propertyOptions, + ...options.stateOptions, }); } }, diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index f443ca9eee..0f95bbae23 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -23,19 +23,19 @@ import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { - css, CSSResult, CSSResultGroup, - html, LitElement, - nothing, PropertyValues, + css, + html, + nothing, } from "lit"; import { customElement, eventOptions, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { guard } from "lit/directives/guard"; import memoizeOne from "memoize-one"; -import { LocalStorage } from "../common/decorators/local-storage"; +import { storage } from "../common/decorators/storage"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { stringCompare } from "../common/string/compare"; @@ -47,10 +47,10 @@ import { subscribeNotifications, } from "../data/persistent_notification"; import { subscribeRepairsIssueRegistry } from "../data/repairs"; -import { updateCanInstall, UpdateEntity } from "../data/update"; +import { UpdateEntity, updateCanInstall } from "../data/update"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; -import { loadSortable, SortableInstance } from "../resources/sortable.ondemand"; +import { SortableInstance, loadSortable } from "../resources/sortable.ondemand"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; import "./ha-icon"; @@ -214,15 +214,17 @@ class HaSidebar extends SubscribeMixin(LitElement) { private sortableStyleLoaded = false; - // @ts-ignore - @LocalStorage("sidebarPanelOrder", true, { - attribute: false, + @storage({ + key: "sidebarPanelOrder", + state: true, + subscribe: true, }) private _panelOrder: string[] = []; - // @ts-ignore - @LocalStorage("sidebarHiddenPanels", true, { - attribute: false, + @storage({ + key: "sidebarHiddenPanels", + state: true, + subscribe: true, }) private _hiddenPanels: string[] = []; diff --git a/src/components/media-player/ha-browse-media-tts.ts b/src/components/media-player/ha-browse-media-tts.ts index d16f2bbe15..39e36b60b7 100644 --- a/src/components/media-player/ha-browse-media-tts.ts +++ b/src/components/media-player/ha-browse-media-tts.ts @@ -1,7 +1,7 @@ import "@material/mwc-list/mwc-list-item"; import { css, html, LitElement, nothing, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { fireEvent } from "../../common/dom/fire_event"; import { MediaPlayerBrowseAction, @@ -43,7 +43,12 @@ class BrowseMediaTTS extends LitElement { @state() private _provider?: TTSEngine; - @LocalStorage("TtsMessage", true, false) private _message!: string; + @storage({ + key: "TtsMessage", + state: true, + subscribe: false, + }) + private _message!: string; protected render() { return html` diff --git a/src/dialogs/tts-try/dialog-tts-try.ts b/src/dialogs/tts-try/dialog-tts-try.ts index 113b5345ca..0854133bba 100644 --- a/src/dialogs/tts-try/dialog-tts-try.ts +++ b/src/dialogs/tts-try/dialog-tts-try.ts @@ -1,7 +1,7 @@ import { mdiPlayCircleOutline } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-button"; import { createCloseHeading } from "../../components/ha-dialog"; @@ -25,10 +25,12 @@ export class TTSTryDialog extends LitElement { @query("#message") private _messageInput?: HaTextArea; - @LocalStorage("ttsTryMessages", false, false) private _messages?: Record< - string, - string - >; + @storage({ + key: "ttsTryMessages", + state: false, + subscribe: false, + }) + private _messages?: Record; public showDialog(params: TTSTryDialogParams) { this._params = params; diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index b3a83bfce5..1aa6695ba2 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -18,7 +18,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; import "../../components/ha-button"; @@ -57,7 +57,12 @@ export class HaVoiceCommandDialog extends LitElement { @state() private _opened = false; - @LocalStorage("AssistPipelineId", true, false) private _pipelineId?: string; + @storage({ + key: "AssistPipelineId", + state: true, + subscribe: false, + }) + private _pipelineId?: string; @state() private _pipeline?: AssistPipeline; diff --git a/src/panels/calendar/ha-panel-calendar.ts b/src/panels/calendar/ha-panel-calendar.ts index 43c824eff9..878d6348fa 100644 --- a/src/panels/calendar/ha-panel-calendar.ts +++ b/src/panels/calendar/ha-panel-calendar.ts @@ -11,7 +11,7 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { HASSDomEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; import "../../components/ha-card"; @@ -41,7 +41,10 @@ class PanelCalendar extends LitElement { @state() private _error?: string = undefined; - @LocalStorage("deSelectedCalendars", true) + @storage({ + key: "deSelectedCalendars", + state: true, + }) private _deSelectedCalendars: string[] = []; private _start?: Date; diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 7324f8d5a4..c8cf4fa69b 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -20,7 +20,7 @@ import { documentationUrl } from "../../../util/documentation-url"; import "./action/ha-automation-action"; import "./condition/ha-automation-condition"; import "./trigger/ha-automation-trigger"; -import { LocalStorage } from "../../../common/decorators/local-storage"; +import { storage } from "../../../common/decorators/storage"; @customElement("manual-automation-editor") export class HaManualAutomationEditor extends LitElement { @@ -36,7 +36,12 @@ export class HaManualAutomationEditor extends LitElement { @property({ attribute: false }) public stateObj?: HassEntity; - @LocalStorage("automationClipboard", true, false, window.sessionStorage) + @storage({ + key: "automationClipboard", + state: true, + subscribe: false, + storage: "sessionStorage", + }) private _clipboard: Clipboard = {}; protected render() { diff --git a/src/panels/config/cloud/account/dialog-cloud-tts-try.ts b/src/panels/config/cloud/account/dialog-cloud-tts-try.ts index effed9dea4..bd079a5854 100644 --- a/src/panels/config/cloud/account/dialog-cloud-tts-try.ts +++ b/src/panels/config/cloud/account/dialog-cloud-tts-try.ts @@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item"; import { mdiPlayCircleOutline, mdiRobot } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { LocalStorage } from "../../../../common/decorators/local-storage"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { computeStateDomain } from "../../../../common/entity/compute_state_domain"; @@ -31,9 +31,19 @@ export class DialogTryTts extends LitElement { @query("#message") private _messageInput?: HaTextArea; - @LocalStorage("cloudTtsTryMessage", false, false) private _message!: string; + @storage({ + key: "cloudTtsTryMessage", + state: false, + subscribe: false, + }) + private _message!: string; - @LocalStorage("cloudTtsTryTarget", false, false) private _target!: string; + @storage({ + key: "cloudTtsTryTarget", + state: false, + subscribe: false, + }) + private _target!: string; public showDialog(params: TryTtsDialogParams) { this._params = params; diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index 297ad0c9d9..032ebf38ed 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts @@ -1,7 +1,7 @@ import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import { LocalStorage } from "../../../../../common/decorators/local-storage"; +import { storage } from "../../../../../common/decorators/storage"; import "../../../../../components/ha-card"; import "../../../../../components/ha-code-editor"; import "../../../../../components/ha-formfield"; @@ -21,19 +21,39 @@ class HaPanelDevMqtt extends LitElement { @property({ type: Boolean }) public narrow!: boolean; - @LocalStorage("panel-dev-mqtt-topic-ls", true, false) + @storage({ + key: "panel-dev-mqtt-topic-ls", + state: true, + subscribe: false, + }) private _topic = ""; - @LocalStorage("panel-dev-mqtt-payload-ls", true, false) + @storage({ + key: "panel-dev-mqtt-payload-ls", + state: true, + subscribe: false, + }) private _payload = ""; - @LocalStorage("panel-dev-mqtt-qos-ls", true, false) + @storage({ + key: "panel-dev-mqtt-qos-ls", + state: true, + subscribe: false, + }) private _qos = "0"; - @LocalStorage("panel-dev-mqtt-retain-ls", true, false) + @storage({ + key: "panel-dev-mqtt-retain-ls", + state: true, + subscribe: false, + }) private _retain = false; - @LocalStorage("panel-dev-mqtt-allow-template-ls", true, false) + @storage({ + key: "panel-dev-mqtt-allow-template-ls", + state: true, + subscribe: false, + }) private _allowTemplate = false; protected render(): TemplateResult { diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts index bb04eb4f78..b1bf864377 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts @@ -8,7 +8,7 @@ import { formatTime } from "../../../../../common/datetime/format_time"; import { MQTTMessage, subscribeMQTTTopic } from "../../../../../data/mqtt"; import { HomeAssistant } from "../../../../../types"; import "@material/mwc-list/mwc-list-item"; -import { LocalStorage } from "../../../../../common/decorators/local-storage"; +import { storage } from "../../../../../common/decorators/storage"; import "../../../../../components/ha-formfield"; import "../../../../../components/ha-switch"; @@ -18,13 +18,25 @@ const qosLevel = ["0", "1", "2"]; class MqttSubscribeCard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @LocalStorage("panel-dev-mqtt-topic-subscribe", true, false) + @storage({ + key: "panel-dev-mqtt-topic-subscribe", + state: true, + subscribe: false, + }) private _topic = ""; - @LocalStorage("panel-dev-mqtt-qos-subscribe", true, false) + @storage({ + key: "panel-dev-mqtt-qos-subscribe", + state: true, + subscribe: false, + }) private _qos = "0"; - @LocalStorage("panel-dev-mqtt-json-format", true, false) + @storage({ + key: "panel-dev-mqtt-json-format", + state: true, + subscribe: false, + }) private _json_format = false; @state() private _subscribed?: () => void; diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index 333c052500..585623dc3b 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -3,7 +3,7 @@ import { mdiHelpCircle } from "@mdi/js"; import deepClone from "deep-clone-simple"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { LocalStorage } from "../../../common/decorators/local-storage"; +import { storage } from "../../../common/decorators/storage"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; @@ -26,7 +26,12 @@ export class HaManualScriptEditor extends LitElement { @property({ attribute: false }) public config!: ScriptConfig; - @LocalStorage("automationClipboard", true, false, window.sessionStorage) + @storage({ + key: "automationClipboard", + state: true, + subscribe: false, + storage: "sessionStorage", + }) private _clipboard: Clipboard = {}; protected render() { diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts index a169b0fb27..6096bd16b4 100644 --- a/src/panels/developer-tools/service/developer-tools-service.ts +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -4,7 +4,7 @@ import { load } from "js-yaml"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { LocalStorage } from "../../../common/decorators/local-storage"; +import { storage } from "../../../common/decorators/storage"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeObjectId } from "../../../common/entity/compute_object_id"; import { hasTemplate } from "../../../common/string/has-template"; @@ -38,10 +38,18 @@ class HaPanelDevService extends LitElement { @state() private _uiAvailable = true; - @LocalStorage("panel-dev-service-state-service-data", true, false) + @storage({ + key: "panel-dev-service-state-service-data", + state: true, + subscribe: false, + }) private _serviceData?: ServiceAction = { service: "", target: {}, data: {} }; - @LocalStorage("panel-dev-service-state-yaml-mode", true, false) + @storage({ + key: "panel-dev-service-state-yaml-mode", + state: true, + subscribe: false, + }) private _yamlMode = false; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index bb3c224750..94d5ab6c15 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -7,7 +7,7 @@ import { import { css, html, LitElement, PropertyValues } from "lit"; import { property, query, state } from "lit/decorators"; import { ensureArray } from "../../common/array/ensure-array"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { navigate } from "../../common/navigate"; import { constructUrlCurrentPath } from "../../common/url/construct-url"; import { @@ -58,7 +58,11 @@ class HaPanelHistory extends SubscribeMixin(LitElement) { @state() private _endDate: Date; - @LocalStorage("historyPickedValue", true, false) + @storage({ + key: "historyPickedValue", + state: true, + subscribe: false, + }) private _targetPickerValue?: HassServiceTarget; @state() private _isLoading = false; diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index 3bd5da6fc9..e71291bec8 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -9,7 +9,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { LocalStorage } from "../../common/decorators/local-storage"; +import { storage } from "../../common/decorators/storage"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { navigate } from "../../common/navigate"; import "../../components/ha-menu-button"; @@ -71,7 +71,11 @@ class PanelMediaBrowser extends LitElement { }, ]; - @LocalStorage("mediaBrowseEntityId", true, false) + @storage({ + key: "mediaBrowseEntityId", + state: true, + subscribe: false, + }) private _entityId = BROWSER_PLAYER; @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;