mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-14 11:17:26 +00:00
Compare commits
9 Commits
dev
...
persist-th
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f35d8ec2 | ||
|
|
3fda44b56d | ||
|
|
362744360f | ||
|
|
3cdc8d61d8 | ||
|
|
27dce6670e | ||
|
|
c2632500ec | ||
|
|
f92635bb5f | ||
|
|
e612bf09d7 | ||
|
|
435d3ee36f |
18
src/data/theme.ts
Normal file
18
src/data/theme.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
theme: ThemeSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export const subscribeThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
callback: (data: { value: ThemeSettings | null }) => void
|
||||
) => subscribeFrontendUserData(hass.connection, "theme", callback);
|
||||
|
||||
export const saveThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
data: ThemeSettings
|
||||
) => saveFrontendUserData(hass.connection, "theme", data);
|
||||
@@ -381,12 +381,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
"ui.panel.config.automation.picker.headers.voice_assistants"
|
||||
),
|
||||
type: "flex",
|
||||
type: "icon",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
template: (automation) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -498,12 +498,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
"ui.panel.config.entities.picker.headers.voice_assistants"
|
||||
),
|
||||
type: "flex",
|
||||
type: "icon",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
template: (entry) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entities,
|
||||
|
||||
@@ -487,12 +487,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
"ui.panel.config.helpers.picker.headers.voice_assistants"
|
||||
),
|
||||
type: "flex",
|
||||
type: "icon",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
template: (helper) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -415,12 +415,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
"ui.panel.config.scene.picker.headers.voice_assistants"
|
||||
),
|
||||
type: "flex",
|
||||
type: "icon",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
template: (scene) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -403,12 +403,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
"ui.panel.config.script.picker.headers.voice_assistants"
|
||||
),
|
||||
type: "flex",
|
||||
type: "icon",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "100px",
|
||||
template: (script) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
|
||||
@@ -15,20 +15,41 @@ import {
|
||||
DefaultAccentColor,
|
||||
DefaultPrimaryColor,
|
||||
} from "../../resources/theme/color/color.globals";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
saveThemePreferences,
|
||||
subscribeThemePreferences,
|
||||
} from "../../data/theme";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { clearSelectedThemeState, getState } from "../../util/ha-pref-storage";
|
||||
|
||||
const USE_DEFAULT_THEME = "__USE_DEFAULT_THEME__";
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
|
||||
@customElement("ha-pick-theme-row")
|
||||
export class HaPickThemeRow extends LitElement {
|
||||
export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() _themeNames: string[] = [];
|
||||
|
||||
@state() private _backendTheme?: ThemeSettings | null;
|
||||
|
||||
@state() private _migrating = false;
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeThemePreferences(this.hass, ({ value }) => {
|
||||
this._backendTheme = value;
|
||||
}).catch(() => {
|
||||
this._backendTheme = undefined;
|
||||
return () => undefined;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hasThemes =
|
||||
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
||||
@@ -41,6 +62,11 @@ export class HaPickThemeRow extends LitElement {
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const themeSettings = this.hass.selectedTheme;
|
||||
const localTheme = this._getLocalTheme();
|
||||
const showMigration =
|
||||
this._backendTheme !== undefined &&
|
||||
this._backendTheme === null &&
|
||||
localTheme !== null;
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
@@ -159,6 +185,28 @@ export class HaPickThemeRow extends LitElement {
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
${showMigration
|
||||
? html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.profile.themes.migrate_header")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.themes.migrate_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
.disabled=${this._migrating}
|
||||
@click=${this._migrateThemePreferences}
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.migrate_button")}
|
||||
</ha-button>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -236,6 +284,31 @@ export class HaPickThemeRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _getLocalTheme(): ThemeSettings | null {
|
||||
return getState().selectedTheme ?? null;
|
||||
}
|
||||
|
||||
private async _migrateThemePreferences() {
|
||||
const localTheme = this._getLocalTheme();
|
||||
if (!localTheme) {
|
||||
return;
|
||||
}
|
||||
this._migrating = true;
|
||||
try {
|
||||
await saveThemePreferences(this.hass, localTheme);
|
||||
clearSelectedThemeState();
|
||||
fireEvent(this, "hass-notification", {
|
||||
message: this.hass.localize("ui.panel.profile.themes.migrate_success"),
|
||||
});
|
||||
} catch (_err: any) {
|
||||
fireEvent(this, "hass-notification", {
|
||||
message: this.hass.localize("ui.panel.profile.themes.migrate_failed"),
|
||||
});
|
||||
} finally {
|
||||
this._migrating = false;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
|
||||
@@ -154,6 +154,10 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
@@ -208,10 +212,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.client_settings_detail")}
|
||||
</div>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
|
||||
@@ -2,7 +2,9 @@ import {
|
||||
applyThemesOnElement,
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { subscribeThemePreferences, saveThemePreferences } from "../data/theme";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
@@ -24,6 +26,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
class extends superClass {
|
||||
private _themeApplied = false;
|
||||
|
||||
private _themePrefsAvailable = false;
|
||||
|
||||
private _themeSaveFailedNotified = false;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("settheme", (ev) => {
|
||||
@@ -34,7 +40,23 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
},
|
||||
});
|
||||
this._applyTheme(mql.matches);
|
||||
storeState(this.hass!);
|
||||
if (this._themePrefsAvailable) {
|
||||
saveThemePreferences(this.hass!, this.hass!.selectedTheme!).catch(
|
||||
() => {
|
||||
storeState(this.hass!);
|
||||
if (!this._themeSaveFailedNotified) {
|
||||
this._themeSaveFailedNotified = true;
|
||||
fireEvent(this, "hass-notification", {
|
||||
message: this.hass!.localize(
|
||||
"ui.notification_toast.theme_save_failed"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
storeState(this.hass!);
|
||||
}
|
||||
});
|
||||
mql.addListener((ev) => this._applyTheme(ev.matches));
|
||||
if (!this._themeApplied && mql.matches) {
|
||||
@@ -63,6 +85,17 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
invalidateThemeCache();
|
||||
this._applyTheme(mql.matches);
|
||||
});
|
||||
|
||||
subscribeThemePreferences(this.hass!, ({ value }) => {
|
||||
this._themePrefsAvailable = true;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
this._updateHass({ selectedTheme: value });
|
||||
this._applyTheme(mql.matches);
|
||||
}).catch(() => {
|
||||
this._themePrefsAvailable = false;
|
||||
});
|
||||
}
|
||||
|
||||
private _applyTheme(darkPreferred: boolean) {
|
||||
|
||||
@@ -2234,7 +2234,8 @@
|
||||
"dismiss": "Dismiss",
|
||||
"no_matching_link_found": "No matching My link found for {path}",
|
||||
"new_version_available": "A new version of the frontend is available.",
|
||||
"reload": "Reload"
|
||||
"reload": "Reload",
|
||||
"theme_save_failed": "Unable to save theme settings to your user profile. Your changes are saved locally to the browser for now."
|
||||
},
|
||||
"sidebar": {
|
||||
"external_app_configuration": "App settings",
|
||||
@@ -3341,7 +3342,8 @@
|
||||
"type": "Type",
|
||||
"editable": "Editable",
|
||||
"category": "Category",
|
||||
"area": "Area"
|
||||
"area": "Area",
|
||||
"voice_assistants": "Voice assistants"
|
||||
},
|
||||
"create_helper": "Create helper",
|
||||
"no_helpers": "Looks like you don't have any helpers yet!",
|
||||
@@ -8945,6 +8947,11 @@
|
||||
"error_no_theme": "No themes available.",
|
||||
"link_promo": "Learn about themes",
|
||||
"dropdown_label": "Theme",
|
||||
"migrate_header": "Migrate theme settings",
|
||||
"migrate_description": "Save your theme selection to your Home Assistant user profile.",
|
||||
"migrate_button": "Migrate",
|
||||
"migrate_success": "Theme settings migrated.",
|
||||
"migrate_failed": "Failed to migrate theme settings.",
|
||||
"dark_mode": {
|
||||
"auto": "Auto",
|
||||
"light": "Light",
|
||||
|
||||
@@ -56,3 +56,11 @@ export function getState(): Partial<StoredHomeAssistant> {
|
||||
export function clearState() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
export function clearSelectedThemeState() {
|
||||
try {
|
||||
window.localStorage.removeItem("selectedTheme");
|
||||
} catch (_err: any) {
|
||||
// Ignore storage errors (private mode, full storage).
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user