From c7ce90883b4ad91e88e9b3ae44d4c19bf4fb8732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Wed, 20 Jul 2022 22:45:03 +0000 Subject: [PATCH] Begun framework for frontend settings across devices --- custom_components/browser_mod/browser_mod.js | 97 ++++++++-- .../browser_mod/browser_mod_panel.js | 168 +++++++++++++++--- custom_components/browser_mod/connection.py | 23 +++ custom_components/browser_mod/store.py | 61 ++++++- js/config_panel/helpers.ts | 20 --- js/config_panel/main.ts | 27 +-- js/config_panel/settings-card.ts | 160 +++++++++++++++++ js/helpers.ts | 21 +++ js/plugin/auto-settings.ts | 25 +++ js/plugin/connection.ts | 64 ++++++- js/plugin/main.ts | 15 +- 11 files changed, 593 insertions(+), 88 deletions(-) delete mode 100644 js/config_panel/helpers.ts create mode 100644 js/config_panel/settings-card.ts create mode 100644 js/plugin/auto-settings.ts diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index 0264b23..623fd64 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -398,13 +398,67 @@ const ConnectionMixin = (SuperClass) => { data: Object.assign(Object.assign({}, this.browsers[this.browserID]), newData), }); } - get meta() { - if (!this.registered) - return null; - return this.browsers[this.browserID].meta; + get global_settings() { + var _a; + const settings = {}; + const global = (_a = this._data.settings) !== null && _a !== void 0 ? _a : {}; + for (const [k, v] of Object.entries(global)) { + if (v !== null) + settings[k] = v; + } + return settings; } - set meta(value) { - this._reregister({ meta: value }); + get user_settings() { + var _a; + const settings = {}; + const user = (_a = this._data.user_settings[this.hass.user.id]) !== null && _a !== void 0 ? _a : {}; + for (const [k, v] of Object.entries(user)) { + if (v !== null) + settings[k] = v; + } + return settings; + } + get browser_settings() { + var _a, _b; + const settings = {}; + const browser = (_b = (_a = this.browsers[this.browserID]) === null || _a === void 0 ? void 0 : _a.settings) !== null && _b !== void 0 ? _b : {}; + for (const [k, v] of Object.entries(browser)) { + if (v !== null) + settings[k] = v; + } + return settings; + } + get settings() { + return Object.assign(Object.assign(Object.assign({}, this.global_settings), this.user_settings), this.browser_settings); + } + set_setting(key, value, level) { + var _a; + switch (level) { + case "global": { + this.connection.sendMessage({ + type: "browser_mod/settings", + key, + value, + }); + break; + } + case "user": { + const user = this.hass.user.id; + this.connection.sendMessage({ + type: "browser_mod/settings", + user, + key, + value, + }); + break; + } + case "browser": { + const settings = (_a = this.browsers[this.browserID]) === null || _a === void 0 ? void 0 : _a.settings; + settings[key] = value; + this._reregister({ settings }); + break; + } + } } get cameraEnabled() { if (!this.registered) @@ -1769,6 +1823,25 @@ __decorate([ customElements.define("popup-card", PopupCard); })(); +const AutoSettingsMixin = (SuperClass) => { + return class AutoSettingsMixinClass extends SuperClass { + constructor() { + super(); + this._auto_settings_setup(); + } + async _auto_settings_setup() { + await this.connectionPromise; + const settings = this.settings; + if (settings.sidebarPanelOrder) { + localStorage.setItem("sidebarPanelOrder", settings.sidebarPanelOrder); + } + if (settings.sidebarHiddenPanels) { + localStorage.setItem("sidebarHiddenPanels", settings.sidebarHiddenPanels); + } + } + }; +}; + /* TODO: - Fix nomenclature @@ -1802,11 +1875,13 @@ __decorate([ x Redesign services to target devices - frontend editor for popup cards - also screensavers - - Tweaks - - Save sidebar - - Save sidebar per user + - Saved frontend settings + X Framework + x Save sidebar - Kiosk mode - - Kiosk mode per user + - Default panel? + - Screensaver? + - Tweaks - Favicon templates - Title templates - Quickbar tweaks (ctrl+enter)? @@ -1814,7 +1889,7 @@ __decorate([ - Media_seek - Screensavers */ -class BrowserMod extends ServicesMixin(PopupMixin(ActivityMixin(BrowserStateMixin(CameraMixin(MediaPlayerMixin(ScreenSaverMixin(FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget)))))))))) { +class BrowserMod extends ServicesMixin(PopupMixin(ActivityMixin(BrowserStateMixin(CameraMixin(MediaPlayerMixin(ScreenSaverMixin(AutoSettingsMixin(FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget))))))))))) { constructor() { super(); this.connect(); diff --git a/custom_components/browser_mod/browser_mod_panel.js b/custom_components/browser_mod/browser_mod_panel.js index 0dfb7fe..cd1e961 100644 --- a/custom_components/browser_mod/browser_mod_panel.js +++ b/custom_components/browser_mod/browser_mod_panel.js @@ -25,7 +25,7 @@ function __decorate(decorators, target, key, desc) { * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const t$1=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,e$3=Symbol(),n$4=new Map;class s$3{constructor(t,n){if(this._$cssResult$=!0,n!==e$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t;}get styleSheet(){let e=n$4.get(this.cssText);return t$1&&void 0===e&&(n$4.set(this.cssText,e=new CSSStyleSheet),e.replaceSync(this.cssText)),e}toString(){return this.cssText}}const o$3=t=>new s$3("string"==typeof t?t:t+"",e$3),r$2=(t,...n)=>{const o=1===t.length?t[0]:n.reduce(((e,n,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+t[s+1]),t[0]);return new s$3(o,e$3)},i$2=(e,n)=>{t$1?e.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((t=>{const n=document.createElement("style"),s=window.litNonce;void 0!==s&&n.setAttribute("nonce",s),n.textContent=t.cssText,e.appendChild(n);}));},S$1=t$1?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return o$3(e)})(t):t; +const t$2=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,e$3=Symbol(),n$4=new Map;class s$3{constructor(t,n){if(this._$cssResult$=!0,n!==e$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t;}get styleSheet(){let e=n$4.get(this.cssText);return t$2&&void 0===e&&(n$4.set(this.cssText,e=new CSSStyleSheet),e.replaceSync(this.cssText)),e}toString(){return this.cssText}}const o$3=t=>new s$3("string"==typeof t?t:t+"",e$3),r$2=(t,...n)=>{const o=1===t.length?t[0]:n.reduce(((e,n,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+t[s+1]),t[0]);return new s$3(o,e$3)},i$2=(e,n)=>{t$2?e.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((t=>{const n=document.createElement("style"),s=window.litNonce;void 0!==s&&n.setAttribute("nonce",s),n.textContent=t.cssText,e.appendChild(n);}));},S$1=t$2?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return o$3(e)})(t):t; /** * @license @@ -38,7 +38,7 @@ const t$1=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeSh * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -var t;const i$1=globalThis.trustedTypes,s$1=i$1?i$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,e$1=`lit$${(Math.random()+"").slice(9)}$`,o$1="?"+e$1,n$2=`<${o$1}>`,l$1=document,h=(t="")=>l$1.createComment(t),r=t=>null===t||"object"!=typeof t&&"function"!=typeof t,d=Array.isArray,u=t=>{var i;return d(t)||"function"==typeof(null===(i=t)||void 0===i?void 0:i[Symbol.iterator])},c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,a=/>/g,f=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,_=/'/g,m=/"/g,g=/^(?:script|style|textarea|title)$/i,p=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),$=p(1),b=Symbol.for("lit-noChange"),w=Symbol.for("lit-nothing"),T=new WeakMap,x=(t,i,s)=>{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(h(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l},A=l$1.createTreeWalker(l$1,129,null,!1),C=(t,i)=>{const o=t.length-1,l=[];let h,r=2===i?"":"",d=c;for(let i=0;i"===u[0]?(d=null!=h?h:c,p=-1):void 0===u[1]?p=-2:(p=d.lastIndex-u[2].length,o=u[1],d=void 0===u[3]?f:'"'===u[3]?m:_):d===m||d===_?d=f:d===v||d===a?d=c:(d=f,h=void 0);const y=d===f&&t[i+1].startsWith("/>")?" ":"";r+=d===c?s+n$2:p>=0?(l.push(o),s.slice(0,p)+"$lit$"+s.slice(p)+e$1+y):s+e$1+(-2===p?(l.push(void 0),i):y);}const u=r+(t[o]||"")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return [void 0!==s$1?s$1.createHTML(u):u,l]};class E{constructor({strings:t,_$litType$:s},n){let l;this.parts=[];let r=0,d=0;const u=t.length-1,c=this.parts,[v,a]=C(t,s);if(this.el=E.createElement(v,n),A.currentNode=this.el.content,2===s){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=i$1?i$1.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=w;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P(this,t,i,0),n=!r(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;lt}):void 0,e$1=`lit$${(Math.random()+"").slice(9)}$`,o$1="?"+e$1,n$2=`<${o$1}>`,l$1=document,h=(t="")=>l$1.createComment(t),r=t=>null===t||"object"!=typeof t&&"function"!=typeof t,d=Array.isArray,u=t=>{var i;return d(t)||"function"==typeof(null===(i=t)||void 0===i?void 0:i[Symbol.iterator])},c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,a=/>/g,f=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,_=/'/g,m=/"/g,g=/^(?:script|style|textarea|title)$/i,p=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),$=p(1),b=Symbol.for("lit-noChange"),w=Symbol.for("lit-nothing"),T=new WeakMap,x=(t,i,s)=>{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(h(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l},A=l$1.createTreeWalker(l$1,129,null,!1),C=(t,i)=>{const o=t.length-1,l=[];let h,r=2===i?"":"",d=c;for(let i=0;i"===u[0]?(d=null!=h?h:c,p=-1):void 0===u[1]?p=-2:(p=d.lastIndex-u[2].length,o=u[1],d=void 0===u[3]?f:'"'===u[3]?m:_):d===m||d===_?d=f:d===v||d===a?d=c:(d=f,h=void 0);const y=d===f&&t[i+1].startsWith("/>")?" ":"";r+=d===c?s+n$2:p>=0?(l.push(o),s.slice(0,p)+"$lit$"+s.slice(p)+e$1+y):s+e$1+(-2===p?(l.push(void 0),i):y);}const u=r+(t[o]||"")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return [void 0!==s$1?s$1.createHTML(u):u,l]};class E{constructor({strings:t,_$litType$:s},n){let l;this.parts=[];let r=0,d=0;const u=t.length-1,c=this.parts,[v,a]=C(t,s);if(this.el=E.createElement(v,n),A.currentNode=this.el.content,2===s){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=i$1?i$1.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=w;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P(this,t,i,0),n=!r(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;l"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(n){n.createProperty(e.key,i);}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this));},finisher(n){n.createProperty(e.key,i);}};function e(e){return (n,t)=>void 0!==t?((i,e,n)=>{e.constructor.createProperty(n,i);})(e,n,t):i(e,n)} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function t(t){return e({...t,state:!0})} + /** * @license * Copyright 2021 Google LLC @@ -81,6 +87,142 @@ const loadDevTools = async () => { await customElements.whenDefined("ha-config-dashboard"); }; +class BrowserModSettingsCard extends s { + constructor() { + super(...arguments); + this._selectedTab = 0; + } + firstUpdated() { + window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate()); + } + _handleSwitchTab(ev) { + this._selectedTab = parseInt(ev.detail.index, 10); + } + render() { + const level = ["browser", "user", "global"][this._selectedTab]; + return $ ` + +
+ + + + + + + ${this._render_settings(level)} +
+
+ `; + } + _render_settings(level) { + const global = window.browser_mod.global_settings; + const user = window.browser_mod.user_settings; + const browser = window.browser_mod.browser_settings; + const current = { global, user, browser }[level]; + const DESC_BOOLEAN = (val) => ({ true: "Enabled", false: "Disabled", undefined: "unset" }[String(val)]); + const DESC_SET_UNSET = (val) => (val === undefined ? "Unset" : "Set"); + const OVERRIDDEN = (key) => { + if (level !== "browser" && browser[key] !== undefined) + return $ `
Overridden by browser setting`; + if (level === "global" && user[key] !== undefined) + return $ `
Overridden by user setting`; + }; + return $ ` + + Kiosk mode + Hide sidebar and header + Currenty: ${DESC_BOOLEAN(current.kiosk)} ${OVERRIDDEN("kiosk")} + + + window.browser_mod.set_setting("kiosk", true, level)} + > + Enable + + window.browser_mod.set_setting("kiosk", false, level)} + > + Disable + + window.browser_mod.set_setting("kiosk", undefined, level)} + > + Clear + + + + + Sidebar order + Order and visibility of sidebar buttons + Currenty: ${DESC_SET_UNSET(current.sidebarPanelOrder)} + ${OVERRIDDEN("sidebarPanelOrder")} + + + { + window.browser_mod.set_setting("sidebarPanelOrder", localStorage.getItem("sidebarPanelOrder"), level); + window.browser_mod.set_setting("sidebarHiddenPanels", localStorage.getItem("sidebarHiddenPanels"), level); + }} + > + Set + + { + window.browser_mod.set_setting("sidebarPanelOrder", undefined, level); + window.browser_mod.set_setting("sidebarHiddenPanels", undefined, level); + }} + > + Clear + + + `; + } + _render_user() { + return $ ` + User + + Kiosk mode + Hide sidebar and header + Currenty: Overridden + + + Set screensaver + Set screensaver card + Enable + Disable + Clear + + `; + } + _render_browser() { + return $ ` + Browser + + Kiosk mode + Hide sidebar and header + Currenty: Overridden + + + Set screensaver + Set screensaver card + Enable + Disable + Clear + + `; + } +} +__decorate([ + e() +], BrowserModSettingsCard.prototype, "hass", void 0); +__decorate([ + t() +], BrowserModSettingsCard.prototype, "_selectedTab", void 0); +customElements.define("browser-mod-settings-card", BrowserModSettingsCard); + const bmWindow = window; loadDevTools().then(() => { class BrowserModPanel extends s { @@ -348,25 +490,9 @@ loadDevTools().then(() => { - -
- - User sidebar - Save sidebar as default for current user - (${this.hass.user.name}) - Save - - - Global sidebar - Save sidebar as default for all users - Save - -
-
+ `; diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index f447fd4..be71359 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -1,4 +1,5 @@ import logging +from typing import Any import voluptuous as vol from datetime import datetime, timezone @@ -130,8 +131,30 @@ async def async_setup_connection(hass): dev = getBrowser(hass, browserID) dev.update(hass, msg.get("data", {})) + @websocket_api.websocket_command( + { + vol.Required("type"): "browser_mod/settings", + vol.Required("key"): str, + vol.Optional("value"): vol.Any(int, str, bool, list, object, None), + vol.Optional("user"): str, + } + ) + @websocket_api.async_response + async def handle_settings(hass, connection, msg): + store = hass.data[DOMAIN]["store"] + if "user" in msg: + # Set user setting + await store.set_user_settings( + msg["user"], **{msg["key"]: msg.get("value", None)} + ) + else: + # Set global setting + await store.set_global_settings(**{msg["key"]: msg.get("value", None)}) + pass + async_register_command(hass, handle_connect) async_register_command(hass, handle_register) async_register_command(hass, handle_unregister) async_register_command(hass, handle_reregister) async_register_command(hass, handle_update) + async_register_command(hass, handle_settings) diff --git a/custom_components/browser_mod/store.py b/custom_components/browser_mod/store.py index 20a5c45..4ceecd5 100644 --- a/custom_components/browser_mod/store.py +++ b/custom_components/browser_mod/store.py @@ -10,11 +10,11 @@ _LOGGER = logging.getLogger(__name__) @attr.s -class BrowserStoreData: - last_seen = attr.ib(type=int, default=0) - enabled = attr.ib(type=bool, default=False) - camera = attr.ib(type=bool, default=False) - meta = attr.ib(type=str, default="default") +class Settings: + kiosk = attr.ib(type=bool, default=None) + defaultPanel = attr.ib(type=str, default=None) + sidebarPanelOrder = attr.ib(type=list, default=None) + sidebarHiddenPanels = attr.ib(type=list, default=None) @classmethod def from_dict(cls, data): @@ -24,21 +24,54 @@ class BrowserStoreData: return attr.asdict(self) +@attr.s +class BrowserStoreData: + last_seen = attr.ib(type=int, default=0) + enabled = attr.ib(type=bool, default=False) + camera = attr.ib(type=bool, default=False) + settings = attr.ib(type=Settings, factory=Settings) + meta = attr.ib(type=str, default="default") + + @classmethod + def from_dict(cls, data): + settings = Settings.from_dict(data.get("settings", {})) + return cls( + **( + data + | { + "settings": settings, + } + ) + ) + + def asdict(self): + return attr.asdict(self) + + @attr.s class ConfigStoreData: browsers = attr.ib(type=dict[str:BrowserStoreData], factory=dict) version = attr.ib(type=str, default="2.0") + settings = attr.ib(type=Settings, factory=Settings) + user_settings = attr.ib(type=dict[str:Settings], factory=dict) @classmethod def from_dict(cls, data={}): browsers = { - k: BrowserStoreData.from_dict(v) for k, v in data["browsers"].items() + k: BrowserStoreData.from_dict(v) + for k, v in data.get("browsers", {}).items() } + user_settings = { + k: Settings.from_dict(v) for k, v in data.get("user_settings", {}).items() + } + settings = Settings.from_dict(data.get("settings", {})) return cls( **( data | { "browsers": browsers, + "settings": settings, + "user_settings": user_settings, } ) ) @@ -97,3 +130,19 @@ class BrowserModStore: async def delete_browser(self, browserID): del self.data.browsers[browserID] await self.updated() + + def get_user_settings(self, name): + return self.data.user_settings.get(name, Settings()) + + async def set_user_settings(self, name, **data): + settings = self.data.user_settings.get(name, Settings()) + settings.__dict__.update(data) + self.data.user_settings[name] = settings + await self.updated() + + def get_global_settings(self): + return self.data.settings + + async def set_global_settings(self, **data): + self.data.settings.__dict__.update(data) + await self.updated() diff --git a/js/config_panel/helpers.ts b/js/config_panel/helpers.ts deleted file mode 100644 index d780231..0000000 --- a/js/config_panel/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Loads in ha-config-dashboard which is used to copy styling -// Also provides ha-settings-row -export const loadDevTools = async () => { - if (customElements.get("ha-config-dashboard")) return; - - await customElements.whenDefined("partial-panel-resolver"); - const ppResolver = document.createElement("partial-panel-resolver"); - const routes = (ppResolver as any).getRoutes([ - { - component_name: "config", - url_path: "a", - }, - ]); - await routes?.routes?.a?.load?.(); - await customElements.whenDefined("ha-panel-config"); - const configRouter = document.createElement("ha-panel-config"); - await (configRouter as any)?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard - await (configRouter as any)?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row - await customElements.whenDefined("ha-config-dashboard"); -}; diff --git a/js/config_panel/main.ts b/js/config_panel/main.ts index 7079421..1e64969 100644 --- a/js/config_panel/main.ts +++ b/js/config_panel/main.ts @@ -1,6 +1,9 @@ import { LitElement, html, css } from "lit"; import { property } from "lit/decorators.js"; -import { loadDevTools } from "./helpers"; +import { loadDevTools } from "../helpers"; +import { loadHaForm } from "../helpers"; + +import "./settings-card"; const bmWindow = window as any; @@ -292,25 +295,9 @@ loadDevTools().then(() => { - -
- - User sidebar - Save sidebar as default for current user - (${this.hass.user.name}) - Save - - - Global sidebar - Save sidebar as default for all users - Save - -
-
+ `; diff --git a/js/config_panel/settings-card.ts b/js/config_panel/settings-card.ts new file mode 100644 index 0000000..4efedfc --- /dev/null +++ b/js/config_panel/settings-card.ts @@ -0,0 +1,160 @@ +import { LitElement, html, css } from "lit"; +import { property, state } from "lit/decorators.js"; + +class BrowserModSettingsCard extends LitElement { + @property() hass; + + @state() _selectedTab = 0; + + firstUpdated() { + window.browser_mod.addEventListener("browser-mod-config-update", () => + this.requestUpdate() + ); + } + + _handleSwitchTab(ev: CustomEvent) { + this._selectedTab = parseInt(ev.detail.index, 10); + } + + render() { + const level = ["browser", "user", "global"][this._selectedTab]; + return html` + +
+ + + + + + + ${this._render_settings(level)} +
+
+ `; + } + + _render_settings(level) { + const global = window.browser_mod.global_settings; + const user = window.browser_mod.user_settings; + const browser = window.browser_mod.browser_settings; + const current = { global, user, browser }[level]; + + const DESC_BOOLEAN = (val) => + ({ true: "Enabled", false: "Disabled", undefined: "unset" }[String(val)]); + const DESC_SET_UNSET = (val) => (val === undefined ? "Unset" : "Set"); + const OVERRIDDEN = (key) => { + if (level !== "browser" && browser[key] !== undefined) + return html`
Overridden by browser setting`; + if (level === "global" && user[key] !== undefined) + return html`
Overridden by user setting`; + }; + + return html` + + Kiosk mode + Hide sidebar and header + Currenty: ${DESC_BOOLEAN(current.kiosk)} ${OVERRIDDEN("kiosk")} + + + window.browser_mod.set_setting("kiosk", true, level)} + > + Enable + + window.browser_mod.set_setting("kiosk", false, level)} + > + Disable + + + window.browser_mod.set_setting("kiosk", undefined, level)} + > + Clear + + + + + Sidebar order + Order and visibility of sidebar buttons + Currenty: ${DESC_SET_UNSET(current.sidebarPanelOrder)} + ${OVERRIDDEN("sidebarPanelOrder")} + + + { + window.browser_mod.set_setting( + "sidebarPanelOrder", + localStorage.getItem("sidebarPanelOrder"), + level + ); + window.browser_mod.set_setting( + "sidebarHiddenPanels", + localStorage.getItem("sidebarHiddenPanels"), + level + ); + }} + > + Set + + { + window.browser_mod.set_setting( + "sidebarPanelOrder", + undefined, + level + ); + window.browser_mod.set_setting( + "sidebarHiddenPanels", + undefined, + level + ); + }} + > + Clear + + + `; + } + + _render_user() { + return html` + User + + Kiosk mode + Hide sidebar and header + Currenty: Overridden + + + Set screensaver + Set screensaver card + Enable + Disable + Clear + + `; + } + + _render_browser() { + return html` + Browser + + Kiosk mode + Hide sidebar and header + Currenty: Overridden + + + Set screensaver + Set screensaver card + Enable + Disable + Clear + + `; + } +} + +customElements.define("browser-mod-settings-card", BrowserModSettingsCard); diff --git a/js/helpers.ts b/js/helpers.ts index 4f0993e..912187d 100644 --- a/js/helpers.ts +++ b/js/helpers.ts @@ -85,3 +85,24 @@ export const loadHaForm = async () => { if (!card) return; await card.getConfigElement(); }; + +// Loads in ha-config-dashboard which is used to copy styling +// Also provides ha-settings-row +export const loadDevTools = async () => { + if (customElements.get("ha-config-dashboard")) return; + + await customElements.whenDefined("partial-panel-resolver"); + const ppResolver = document.createElement("partial-panel-resolver"); + const routes = (ppResolver as any).getRoutes([ + { + component_name: "config", + url_path: "a", + }, + ]); + await routes?.routes?.a?.load?.(); + await customElements.whenDefined("ha-panel-config"); + const configRouter = document.createElement("ha-panel-config"); + await (configRouter as any)?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard + await (configRouter as any)?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row + await customElements.whenDefined("ha-config-dashboard"); +}; diff --git a/js/plugin/auto-settings.ts b/js/plugin/auto-settings.ts new file mode 100644 index 0000000..287f731 --- /dev/null +++ b/js/plugin/auto-settings.ts @@ -0,0 +1,25 @@ +export const AutoSettingsMixin = (SuperClass) => { + return class AutoSettingsMixinClass extends SuperClass { + constructor() { + super(); + + this._auto_settings_setup(); + } + + async _auto_settings_setup() { + await this.connectionPromise; + + const settings = this.settings; + + if (settings.sidebarPanelOrder) { + localStorage.setItem("sidebarPanelOrder", settings.sidebarPanelOrder); + } + if (settings.sidebarHiddenPanels) { + localStorage.setItem( + "sidebarHiddenPanels", + settings.sidebarHiddenPanels + ); + } + } + }; +}; diff --git a/js/plugin/connection.ts b/js/plugin/connection.ts index b219ba2..44f81ac 100644 --- a/js/plugin/connection.ts +++ b/js/plugin/connection.ts @@ -119,12 +119,66 @@ export const ConnectionMixin = (SuperClass) => { }); } - get meta() { - if (!this.registered) return null; - return this.browsers[this.browserID].meta; + get global_settings() { + const settings = {}; + const global = this._data.settings ?? {}; + for (const [k, v] of Object.entries(global)) { + if (v !== null) settings[k] = v; + } + return settings; } - set meta(value) { - this._reregister({ meta: value }); + get user_settings() { + const settings = {}; + const user = this._data.user_settings[this.hass.user.id] ?? {}; + for (const [k, v] of Object.entries(user)) { + if (v !== null) settings[k] = v; + } + return settings; + } + get browser_settings() { + const settings = {}; + const browser = this.browsers[this.browserID]?.settings ?? {}; + for (const [k, v] of Object.entries(browser)) { + if (v !== null) settings[k] = v; + } + return settings; + } + + get settings() { + return { + ...this.global_settings, + ...this.user_settings, + ...this.browser_settings, + }; + } + + set_setting(key, value, level) { + switch (level) { + case "global": { + this.connection.sendMessage({ + type: "browser_mod/settings", + key, + value, + }); + break; + } + case "user": { + const user = this.hass.user.id; + this.connection.sendMessage({ + type: "browser_mod/settings", + user, + key, + value, + }); + break; + } + case "browser": { + const settings = this.browsers[this.browserID]?.settings; + settings[key] = value; + this._reregister({ settings }); + break; + } + } } get cameraEnabled() { diff --git a/js/plugin/main.ts b/js/plugin/main.ts index 11879a4..1aef3b2 100644 --- a/js/plugin/main.ts +++ b/js/plugin/main.ts @@ -14,6 +14,7 @@ import "./popups"; import { PopupMixin } from "./popups"; import pjson from "../../package.json"; import "./popup-card"; +import { AutoSettingsMixin } from "./auto-settings"; /* TODO: @@ -48,11 +49,13 @@ import "./popup-card"; x Redesign services to target devices - frontend editor for popup cards - also screensavers - - Tweaks - - Save sidebar - - Save sidebar per user + - Saved frontend settings + X Framework + x Save sidebar - Kiosk mode - - Kiosk mode per user + - Default panel? + - Screensaver? + - Tweaks - Favicon templates - Title templates - Quickbar tweaks (ctrl+enter)? @@ -67,7 +70,9 @@ export class BrowserMod extends ServicesMixin( CameraMixin( MediaPlayerMixin( ScreenSaverMixin( - FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget))) + AutoSettingsMixin( + FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget))) + ) ) ) )