Begun framework for frontend settings across devices

This commit is contained in:
Thomas Lovén 2022-07-20 22:45:03 +00:00
parent 77554aa86c
commit c7ce90883b
11 changed files with 593 additions and 88 deletions

View File

@ -398,13 +398,67 @@ const ConnectionMixin = (SuperClass) => {
data: Object.assign(Object.assign({}, this.browsers[this.browserID]), newData), data: Object.assign(Object.assign({}, this.browsers[this.browserID]), newData),
}); });
} }
get meta() { get global_settings() {
if (!this.registered) var _a;
return null; const settings = {};
return this.browsers[this.browserID].meta; 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) { get user_settings() {
this._reregister({ meta: value }); 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() { get cameraEnabled() {
if (!this.registered) if (!this.registered)
@ -1769,6 +1823,25 @@ __decorate([
customElements.define("popup-card", PopupCard); 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: TODO:
- Fix nomenclature - Fix nomenclature
@ -1802,11 +1875,13 @@ __decorate([
x Redesign services to target devices x Redesign services to target devices
- frontend editor for popup cards - frontend editor for popup cards
- also screensavers - also screensavers
- Tweaks - Saved frontend settings
- Save sidebar X Framework
- Save sidebar per user x Save sidebar
- Kiosk mode - Kiosk mode
- Kiosk mode per user - Default panel?
- Screensaver?
- Tweaks
- Favicon templates - Favicon templates
- Title templates - Title templates
- Quickbar tweaks (ctrl+enter)? - Quickbar tweaks (ctrl+enter)?
@ -1814,7 +1889,7 @@ __decorate([
- Media_seek - Media_seek
- Screensavers - 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() { constructor() {
super(); super();
this.connect(); this.connect();

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import Any
import voluptuous as vol import voluptuous as vol
from datetime import datetime, timezone from datetime import datetime, timezone
@ -130,8 +131,30 @@ async def async_setup_connection(hass):
dev = getBrowser(hass, browserID) dev = getBrowser(hass, browserID)
dev.update(hass, msg.get("data", {})) 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_connect)
async_register_command(hass, handle_register) async_register_command(hass, handle_register)
async_register_command(hass, handle_unregister) async_register_command(hass, handle_unregister)
async_register_command(hass, handle_reregister) async_register_command(hass, handle_reregister)
async_register_command(hass, handle_update) async_register_command(hass, handle_update)
async_register_command(hass, handle_settings)

View File

@ -10,11 +10,11 @@ _LOGGER = logging.getLogger(__name__)
@attr.s @attr.s
class BrowserStoreData: class Settings:
last_seen = attr.ib(type=int, default=0) kiosk = attr.ib(type=bool, default=None)
enabled = attr.ib(type=bool, default=False) defaultPanel = attr.ib(type=str, default=None)
camera = attr.ib(type=bool, default=False) sidebarPanelOrder = attr.ib(type=list, default=None)
meta = attr.ib(type=str, default="default") sidebarHiddenPanels = attr.ib(type=list, default=None)
@classmethod @classmethod
def from_dict(cls, data): def from_dict(cls, data):
@ -24,21 +24,54 @@ class BrowserStoreData:
return attr.asdict(self) 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 @attr.s
class ConfigStoreData: class ConfigStoreData:
browsers = attr.ib(type=dict[str:BrowserStoreData], factory=dict) browsers = attr.ib(type=dict[str:BrowserStoreData], factory=dict)
version = attr.ib(type=str, default="2.0") 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 @classmethod
def from_dict(cls, data={}): def from_dict(cls, data={}):
browsers = { 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( return cls(
**( **(
data data
| { | {
"browsers": browsers, "browsers": browsers,
"settings": settings,
"user_settings": user_settings,
} }
) )
) )
@ -97,3 +130,19 @@ class BrowserModStore:
async def delete_browser(self, browserID): async def delete_browser(self, browserID):
del self.data.browsers[browserID] del self.data.browsers[browserID]
await self.updated() 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()

View File

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

View File

@ -1,6 +1,9 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { property } from "lit/decorators.js"; 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; const bmWindow = window as any;
@ -292,25 +295,9 @@ loadDevTools().then(() => {
</div> </div>
</ha-card> </ha-card>
<ha-card outlined header="Tweaks"> <browser-mod-settings-card
<div class="card-content"> .hass=${this.hass}
<ha-settings-row> ></browser-mod-settings-card>
<span slot="heading">User sidebar</span>
<span slot="description"
>Save sidebar as default for current user
(${this.hass.user.name})</span
>
<mwc-button>Save</mwc-button>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Global sidebar</span>
<span slot="description"
>Save sidebar as default for all users</span
>
<mwc-button>Save</mwc-button>
</ha-settings-row>
</div>
</ha-card>
</ha-config-section> </ha-config-section>
</ha-app-layout> </ha-app-layout>
`; `;

View File

@ -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`
<ha-card header="Auto settings" outlined>
<div class="card-content">
<mwc-tab-bar
.activeIndex=${this._selectedTab}
@MDCTabBar:activated=${this._handleSwitchTab}
>
<mwc-tab .label=${"Browser"}></mwc-tab>
<mwc-tab .label=${"User (" + this.hass.user.name + ")"}></mwc-tab>
<mwc-tab .label=${"Global"}></mwc-tab>
</mwc-tab-bar>
${this._render_settings(level)}
</div>
</ha-card>
`;
}
_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`<br />Overridden by browser setting`;
if (level === "global" && user[key] !== undefined)
return html`<br />Overridden by user setting`;
};
return html`
<ha-settings-row>
<span slot="heading">Kiosk mode</span>
<span slot="description"> Hide sidebar and header </span>
Currenty: ${DESC_BOOLEAN(current.kiosk)} ${OVERRIDDEN("kiosk")}
</ha-settings-row>
<ha-settings-row>
<mwc-button
@click=${() => window.browser_mod.set_setting("kiosk", true, level)}
>
Enable
</mwc-button>
<mwc-button
@click=${() => window.browser_mod.set_setting("kiosk", false, level)}
>
Disable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("kiosk", undefined, level)}
>
Clear
</mwc-button>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Sidebar order</span>
<span slot="description">Order and visibility of sidebar buttons</span>
Currenty: ${DESC_SET_UNSET(current.sidebarPanelOrder)}
${OVERRIDDEN("sidebarPanelOrder")}
</ha-settings-row>
<ha-settings-row>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
localStorage.getItem("sidebarPanelOrder"),
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
localStorage.getItem("sidebarHiddenPanels"),
level
);
}}
>
Set
</mwc-button>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
undefined,
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
undefined,
level
);
}}
>
Clear
</mwc-button>
</ha-settings-row>
`;
}
_render_user() {
return html`
User
<ha-settings-row>
<span slot="heading">Kiosk mode</span>
<span slot="description"> Hide sidebar and header </span>
Currenty: Overridden
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Set screensaver</span>
<span slot="description"> Set screensaver card </span>
<mwc-button>Enable</mwc-button>
<mwc-button>Disable</mwc-button>
<mwc-button>Clear</mwc-button>
</ha-settings-row>
`;
}
_render_browser() {
return html`
Browser
<ha-settings-row>
<span slot="heading">Kiosk mode</span>
<span slot="description"> Hide sidebar and header </span>
Currenty: Overridden
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Set screensaver</span>
<span slot="description"> Set screensaver card </span>
<mwc-button>Enable</mwc-button>
<mwc-button>Disable</mwc-button>
<mwc-button>Clear</mwc-button>
</ha-settings-row>
`;
}
}
customElements.define("browser-mod-settings-card", BrowserModSettingsCard);

View File

@ -85,3 +85,24 @@ export const loadHaForm = async () => {
if (!card) return; if (!card) return;
await card.getConfigElement(); 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");
};

View File

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

View File

@ -119,12 +119,66 @@ export const ConnectionMixin = (SuperClass) => {
}); });
} }
get meta() { get global_settings() {
if (!this.registered) return null; const settings = {};
return this.browsers[this.browserID].meta; const global = this._data.settings ?? {};
for (const [k, v] of Object.entries(global)) {
if (v !== null) settings[k] = v;
}
return settings;
} }
set meta(value) { get user_settings() {
this._reregister({ meta: value }); 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() { get cameraEnabled() {

View File

@ -14,6 +14,7 @@ import "./popups";
import { PopupMixin } from "./popups"; import { PopupMixin } from "./popups";
import pjson from "../../package.json"; import pjson from "../../package.json";
import "./popup-card"; import "./popup-card";
import { AutoSettingsMixin } from "./auto-settings";
/* /*
TODO: TODO:
@ -48,11 +49,13 @@ import "./popup-card";
x Redesign services to target devices x Redesign services to target devices
- frontend editor for popup cards - frontend editor for popup cards
- also screensavers - also screensavers
- Tweaks - Saved frontend settings
- Save sidebar X Framework
- Save sidebar per user x Save sidebar
- Kiosk mode - Kiosk mode
- Kiosk mode per user - Default panel?
- Screensaver?
- Tweaks
- Favicon templates - Favicon templates
- Title templates - Title templates
- Quickbar tweaks (ctrl+enter)? - Quickbar tweaks (ctrl+enter)?
@ -67,7 +70,9 @@ export class BrowserMod extends ServicesMixin(
CameraMixin( CameraMixin(
MediaPlayerMixin( MediaPlayerMixin(
ScreenSaverMixin( ScreenSaverMixin(
FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget))) AutoSettingsMixin(
FullyMixin(RequireInteractMixin(ConnectionMixin(EventTarget)))
)
) )
) )
) )