Add support for custom themes to use dark mode (#8347)

This commit is contained in:
Philip Allgaier 2021-05-25 13:26:35 +02:00 committed by GitHub
parent 8af05e2726
commit 7f75ca81f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 140 deletions

View File

@ -103,25 +103,27 @@ export class HassioMain extends SupervisorBaseElement {
private _applyTheme() { private _applyTheme() {
let themeName: string; let themeName: string;
let options: Partial<HomeAssistant["selectedTheme"]> | undefined; let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) { if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName = themeName =
this.hass.selectedTheme?.theme || this.hass.selectedThemeSettings?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme (this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme! ? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
options = this.hass.selectedTheme; themeSettings = this.hass.selectedThemeSettings;
if (themeName === "default" && options?.dark === undefined) { if (themeSettings?.dark === undefined) {
options = { themeSettings = {
...this.hass.selectedTheme, ...this.hass.selectedThemeSettings,
dark: this.hass.themes.darkMode, dark: this.hass.themes.darkMode,
}; };
} }
} else { } else {
themeName = themeName =
((this.hass.selectedTheme as unknown) as string) || ((this.hass.selectedThemeSettings as unknown) as string) ||
this.hass.themes.default_theme; this.hass.themes.default_theme;
} }
@ -129,7 +131,7 @@ export class HassioMain extends SupervisorBaseElement {
this.parentElement, this.parentElement,
this.hass.themes, this.hass.themes,
themeName, themeName,
options themeSettings
); );
} }
} }

View File

@ -1,4 +1,4 @@
import { Theme } from "../../data/ws-themes"; import { ThemeVars } from "../../data/ws-themes";
import { darkStyles, derivedStyles } from "../../resources/styles"; import { darkStyles, derivedStyles } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { import {
@ -23,50 +23,60 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
* Apply a theme to an element by setting the CSS variables on it. * Apply a theme to an element by setting the CSS variables on it.
* *
* element: Element to apply theme on. * element: Element to apply theme on.
* themes: HASS Theme information * themes: HASS theme information.
* selectedTheme: selected theme. * selectedTheme: Selected theme.
* themeSettings: Settings such as selected dark mode and colors.
*/ */
export const applyThemesOnElement = ( export const applyThemesOnElement = (
element, element,
themes: HomeAssistant["themes"], themes: HomeAssistant["themes"],
selectedTheme?: string, selectedTheme?: string,
themeOptions?: Partial<HomeAssistant["selectedTheme"]> themeSettings?: Partial<HomeAssistant["selectedThemeSettings"]>
) => { ) => {
let cacheKey = selectedTheme; let cacheKey = selectedTheme;
let themeRules: Partial<Theme> = {}; let themeRules: Partial<ThemeVars> = {};
if (selectedTheme === "default" && themeOptions) { if (themeSettings) {
if (themeOptions.dark) { if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`; cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles; themeRules = darkStyles;
if (themeOptions.primaryColor) { }
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend( themeRules["app-header-background-color"] = hexBlend(
themeOptions.primaryColor, primaryColor,
"#121212", "#121212",
8 8
); );
} }
}
if (themeOptions.primaryColor) { if (primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`; cacheKey = `${cacheKey}__primary_${primaryColor}`;
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor); const rgbPrimaryColor = hex2rgb(primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor); const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = themeOptions.primaryColor; themeRules["primary-color"] = primaryColor;
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor)); const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor); themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor)); themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] = themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121"; rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] = themeRules["text-light-primary-color"] =
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6 rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff" ? "#fff"
: "#212121"; : "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"]; themeRules["state-icon-color"] = themeRules["dark-primary-color"];
} }
if (themeOptions.accentColor) { if (accentColor) {
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`; cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = themeOptions.accentColor; themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(themeOptions.accentColor); const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] = themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121"; rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
} }
@ -76,9 +86,27 @@ export const applyThemesOnElement = (
return; return;
} }
} }
}
if (selectedTheme && themes.themes[selectedTheme]) { // Custom theme logic (not relevant for default theme, since it would override
themeRules = themes.themes[selectedTheme]; // the derived calculations from above)
if (
selectedTheme &&
selectedTheme !== "default" &&
themes.themes[selectedTheme]
) {
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
themeRules = { ...themeRules, ...baseThemeRules };
// Apply theme vars for the specific mode if available
if (modes) {
if (themeSettings?.dark) {
themeRules = { ...themeRules, ...modes.dark };
} else {
themeRules = { ...themeRules, ...modes.light };
}
}
} }
if (!element._themes?.keys && !Object.keys(themeRules).length) { if (!element._themes?.keys && !Object.keys(themeRules).length) {
@ -106,12 +134,12 @@ export const applyThemesOnElement = (
const processTheme = ( const processTheme = (
cacheKey: string, cacheKey: string,
theme: Partial<Theme> theme: Partial<ThemeVars>
): ProcessedTheme | undefined => { ): ProcessedTheme | undefined => {
if (!theme || !Object.keys(theme).length) { if (!theme || !Object.keys(theme).length) {
return undefined; return undefined;
} }
const combinedTheme: Partial<Theme> = { const combinedTheme: Partial<ThemeVars> = {
...derivedStyles, ...derivedStyles,
...theme, ...theme,
}; };

View File

@ -108,7 +108,7 @@ class LocationEditor extends LitElement {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) { if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return; return;
} }
if (!this._leafletMap || !this._tileLayer) { if (!this._leafletMap || !this._tileLayer) {
@ -118,7 +118,7 @@ class LocationEditor extends LitElement {
this.Leaflet, this.Leaflet,
this._leafletMap, this._leafletMap,
this._tileLayer, this._tileLayer,
this.hass.themes?.darkMode this.hass.themes.darkMode
); );
} }
} }
@ -130,7 +130,7 @@ class LocationEditor extends LitElement {
private async _initMap(): Promise<void> { private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl, this._mapEl,
this.darkMode ?? this.hass.themes?.darkMode, this.darkMode ?? this.hass.themes.darkMode,
Boolean(this.radius) Boolean(this.radius)
); );
this._leafletMap.addEventListener( this._leafletMap.addEventListener(

View File

@ -1,6 +1,6 @@
import { Connection, createCollection } from "home-assistant-js-websocket"; import { Connection, createCollection } from "home-assistant-js-websocket";
export interface Theme { export interface ThemeVars {
// Incomplete // Incomplete
"primary-color": string; "primary-color": string;
"text-primary-color": string; "text-primary-color": string;
@ -8,10 +8,20 @@ export interface Theme {
[key: string]: string; [key: string]: string;
} }
export type Theme = ThemeVars & {
modes?: {
light?: ThemeVars;
dark?: ThemeVars;
};
};
export interface Themes { export interface Themes {
default_theme: string; default_theme: string;
default_dark_theme: string | null; default_dark_theme: string | null;
themes: Record<string, Theme>; themes: Record<string, Theme>;
// Currently effective dark mode. Will never be undefined. If user selected "auto"
// in theme picker, this property will still contain either true or false based on
// what has been determined via system preferences and support from the selected theme.
darkMode: boolean; darkMode: boolean;
} }

View File

@ -1,8 +1,12 @@
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import {
DEFAULT_ACCENT_COLOR,
DEFAULT_PRIMARY_COLOR,
} from "../resources/ha-style";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export const defaultRadiusColor = "#FF9800"; export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
export const homeRadiusColor = "#03a9f4"; export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
export const passiveRadiusColor = "#9b9b9b"; export const passiveRadiusColor = "#9b9b9b";
export interface Zone { export interface Zone {

View File

@ -277,7 +277,7 @@ export const provideHass = (
mockTheme(theme) { mockTheme(theme) {
invalidateThemeCache(); invalidateThemeCache();
hass().updateHass({ hass().updateHass({
selectedTheme: { theme: theme ? "mock" : "default" }, selectedThemeSettings: { theme: theme ? "mock" : "default" },
themes: { themes: {
...hass().themes, ...hass().themes,
themes: { themes: {
@ -285,11 +285,11 @@ export const provideHass = (
}, },
}, },
}); });
const { themes, selectedTheme } = hass(); const { themes, selectedThemeSettings } = hass();
applyThemesOnElement( applyThemesOnElement(
document.documentElement, document.documentElement,
themes, themes,
selectedTheme!.theme selectedThemeSettings!.theme
); );
}, },

View File

@ -81,25 +81,27 @@ class SupervisorErrorScreen extends LitElement {
private _applyTheme() { private _applyTheme() {
let themeName: string; let themeName: string;
let options: Partial<HomeAssistant["selectedTheme"]> | undefined; let themeSettings:
| Partial<HomeAssistant["selectedThemeSettings"]>
| undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) { if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName = themeName =
this.hass.selectedTheme?.theme || this.hass.selectedThemeSettings?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme (this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme! ? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
options = this.hass.selectedTheme; themeSettings = this.hass.selectedThemeSettings;
if (themeName === "default" && options?.dark === undefined) { if (themeName === "default" && themeSettings?.dark === undefined) {
options = { themeSettings = {
...this.hass.selectedTheme, ...this.hass.selectedThemeSettings,
dark: this.hass.themes.darkMode, dark: this.hass.themes.darkMode,
}; };
} }
} else { } else {
themeName = themeName =
((this.hass.selectedTheme as unknown) as string) || ((this.hass.selectedThemeSettings as unknown) as string) ||
this.hass.themes.default_theme; this.hass.themes.default_theme;
} }
@ -107,7 +109,7 @@ class SupervisorErrorScreen extends LitElement {
this.parentElement, this.parentElement,
this.hass.themes, this.hass.themes,
themeName, themeName,
options themeSettings
); );
} }

View File

@ -152,7 +152,7 @@ export class HUIView extends ReactiveElement {
changedProperties.has("hass") && changedProperties.has("hass") &&
(!oldHass || (!oldHass ||
this.hass.themes !== oldHass.themes || this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme) this.hass.selectedThemeSettings !== oldHass.selectedThemeSettings)
) { ) {
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme); applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
} }

View File

@ -17,6 +17,11 @@ import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-radio"; import "../../components/ha-radio";
import type { HaRadio } from "../../components/ha-radio"; import type { HaRadio } from "../../components/ha-radio";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import { Theme } from "../../data/ws-themes";
import {
DEFAULT_PRIMARY_COLOR,
DEFAULT_ACCENT_COLOR,
} from "../../resources/ha-style";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
@ -26,15 +31,20 @@ export class HaPickThemeRow extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@state() _themes: string[] = []; @state() _themeNames: string[] = [];
@state() _selectedTheme = 0; @state() _selectedThemeIndex = 0;
@state() _selectedTheme?: Theme;
protected render(): TemplateResult { protected render(): TemplateResult {
const hasThemes = const hasThemes =
this.hass.themes?.themes && Object.keys(this.hass.themes.themes).length; this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
const curTheme = const curTheme =
this.hass!.selectedTheme?.theme || this.hass!.themes.default_theme; this.hass.selectedThemeSettings?.theme || this.hass.themes.default_theme;
const themeSettings = this.hass.selectedThemeSettings;
return html` return html`
<ha-settings-row .narrow=${this.narrow}> <ha-settings-row .narrow=${this.narrow}>
<span slot="heading" <span slot="heading"
@ -46,7 +56,7 @@ export class HaPickThemeRow extends LitElement {
: ""} : ""}
<a <a
href="${documentationUrl( href="${documentationUrl(
this.hass!, this.hass,
"/integrations/frontend/#defining-themes" "/integrations/frontend/#defining-themes"
)}" )}"
target="_blank" target="_blank"
@ -62,19 +72,20 @@ export class HaPickThemeRow extends LitElement {
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${this._selectedTheme} .selected=${this._selectedThemeIndex}
@iron-select=${this._handleThemeSelection} @iron-select=${this._handleThemeSelection}
> >
${this._themes.map( ${this._themeNames.map(
(theme) => html`<paper-item .theme=${theme}>${theme}</paper-item>` (theme) => html`<paper-item .theme=${theme}>${theme}</paper-item>`
)} )}
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
</ha-settings-row> </ha-settings-row>
${curTheme === "default" ${curTheme === "default" ||
(this._selectedTheme && this._supportsModeSelection(this._selectedTheme))
? html` <div class="inputs"> ? html` <div class="inputs">
<ha-formfield <ha-formfield
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.profile.themes.dark_mode.auto" "ui.panel.profile.themes.dark_mode.auto"
)} )}
> >
@ -82,11 +93,11 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="auto" value="auto"
?checked=${this.hass.selectedTheme?.dark === undefined} ?checked=${themeSettings?.dark === undefined}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.profile.themes.dark_mode.light" "ui.panel.profile.themes.dark_mode.light"
)} )}
> >
@ -94,12 +105,12 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="light" value="light"
?checked=${this.hass.selectedTheme?.dark === false} ?checked=${themeSettings?.dark === false}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.profile.themes.dark_mode.dark" "ui.panel.profile.themes.dark_mode.dark"
)} )}
> >
@ -107,36 +118,38 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="dark" value="dark"
?checked=${this.hass.selectedTheme?.dark === true} ?checked=${themeSettings?.dark === true}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
<div class="color-pickers"> ${curTheme === "default"
? html` <div class="color-pickers">
<paper-input <paper-input
.value=${this.hass!.selectedTheme?.primaryColor || "#03a9f4"} .value=${themeSettings?.primaryColor ||
DEFAULT_PRIMARY_COLOR}
type="color" type="color"
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.profile.themes.primary_color" "ui.panel.profile.themes.primary_color"
)} )}
.name=${"primaryColor"} .name=${"primaryColor"}
@change=${this._handleColorChange} @change=${this._handleColorChange}
></paper-input> ></paper-input>
<paper-input <paper-input
.value=${this.hass!.selectedTheme?.accentColor || "#ff9800"} .value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
type="color" type="color"
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.profile.themes.accent_color" "ui.panel.profile.themes.accent_color"
)} )}
.name=${"accentColor"} .name=${"accentColor"}
@change=${this._handleColorChange} @change=${this._handleColorChange}
></paper-input> ></paper-input>
${this.hass!.selectedTheme?.primaryColor || ${themeSettings?.primaryColor || themeSettings?.accentColor
this.hass!.selectedTheme?.accentColor
? html` <mwc-button @click=${this._resetColors}> ? html` <mwc-button @click=${this._resetColors}>
${this.hass!.localize("ui.panel.profile.themes.reset")} ${this.hass.localize("ui.panel.profile.themes.reset")}
</mwc-button>` </mwc-button>`
: ""} : ""}
</div> </div>`
: ""}
</div>` </div>`
: ""} : ""}
`; `;
@ -146,27 +159,31 @@ export class HaPickThemeRow extends LitElement {
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant; const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
const themesChanged = const themesChanged =
changedProperties.has("hass") && changedProperties.has("hass") &&
(!oldHass || oldHass.themes?.themes !== this.hass.themes?.themes); (!oldHass || oldHass.themes.themes !== this.hass.themes.themes);
const selectedThemeChanged = const selectedThemeChanged =
changedProperties.has("hass") && changedProperties.has("hass") &&
(!oldHass || oldHass.selectedTheme !== this.hass.selectedTheme); (!oldHass ||
oldHass.selectedThemeSettings !== this.hass.selectedThemeSettings);
if (themesChanged) { if (themesChanged) {
this._themes = ["Backend-selected", "default"].concat( this._themeNames = ["Backend-selected", "default"].concat(
Object.keys(this.hass.themes.themes).sort() Object.keys(this.hass.themes.themes).sort()
); );
} }
if (selectedThemeChanged) { if (selectedThemeChanged) {
if ( if (
this.hass.selectedTheme && this.hass.selectedThemeSettings &&
this._themes.indexOf(this.hass.selectedTheme.theme) > 0 this._themeNames.indexOf(this.hass.selectedThemeSettings.theme) > 0
) { ) {
this._selectedTheme = this._themes.indexOf( this._selectedThemeIndex = this._themeNames.indexOf(
this.hass.selectedTheme.theme this.hass.selectedThemeSettings.theme
); );
} else if (!this.hass.selectedTheme) { this._selectedTheme = this.hass.themes.themes[
this._selectedTheme = 0; this.hass.selectedThemeSettings.theme
];
} else if (!this.hass.selectedThemeSettings) {
this._selectedThemeIndex = 0;
} }
} }
} }
@ -183,6 +200,10 @@ export class HaPickThemeRow extends LitElement {
}); });
} }
private _supportsModeSelection(theme: Theme): boolean {
return theme.modes?.light !== undefined && theme.modes?.dark !== undefined;
}
private _handleDarkMode(ev: CustomEvent) { private _handleDarkMode(ev: CustomEvent) {
let dark: boolean | undefined; let dark: boolean | undefined;
switch ((ev.target as HaRadio).value) { switch ((ev.target as HaRadio).value) {
@ -199,12 +220,20 @@ export class HaPickThemeRow extends LitElement {
private _handleThemeSelection(ev: CustomEvent) { private _handleThemeSelection(ev: CustomEvent) {
const theme = ev.detail.item.theme; const theme = ev.detail.item.theme;
if (theme === "Backend-selected") { if (theme === "Backend-selected") {
if (this.hass.selectedTheme?.theme) { if (this.hass.selectedThemeSettings?.theme) {
fireEvent(this, "settheme", { theme: "" }); fireEvent(this, "settheme", {
theme: "",
primaryColor: undefined,
accentColor: undefined,
});
} }
return; return;
} }
fireEvent(this, "settheme", { theme }); fireEvent(this, "settheme", {
theme,
primaryColor: undefined,
accentColor: undefined,
});
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -2,6 +2,9 @@ import "@polymer/paper-styles/paper-styles";
import "@polymer/polymer/lib/elements/custom-style"; import "@polymer/polymer/lib/elements/custom-style";
import { derivedStyles } from "./styles"; import { derivedStyles } from "./styles";
export const DEFAULT_PRIMARY_COLOR = "#03a9f4";
export const DEFAULT_ACCENT_COLOR = "#ff9800";
const documentContainer = document.createElement("template"); const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;"); documentContainer.setAttribute("style", "display: none;");

View File

@ -39,6 +39,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
states: null as any, states: null as any,
config: null as any, config: null as any,
themes: null as any, themes: null as any,
selectedThemeSettings: null,
panels: null as any, panels: null as any,
services: null as any, services: null as any,
user: null as any, user: null as any,

View File

@ -11,10 +11,10 @@ import { HassBaseEl } from "./hass-base-mixin";
declare global { declare global {
// for add event listener // for add event listener
interface HTMLElementEventMap { interface HTMLElementEventMap {
settheme: HASSDomEvent<Partial<HomeAssistant["selectedTheme"]>>; settheme: HASSDomEvent<Partial<HomeAssistant["selectedThemeSettings"]>>;
} }
interface HASSDomEvents { interface HASSDomEvents {
settheme: Partial<HomeAssistant["selectedTheme"]>; settheme: Partial<HomeAssistant["selectedThemeSettings"]>;
} }
} }
@ -28,7 +28,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("settheme", (ev) => { this.addEventListener("settheme", (ev) => {
this._updateHass({ this._updateHass({
selectedTheme: { ...this.hass!.selectedTheme!, ...ev.detail }, selectedThemeSettings: {
...this.hass!.selectedThemeSettings!,
...ev.detail,
},
}); });
this._applyTheme(mql.matches); this._applyTheme(mql.matches);
storeState(this.hass!); storeState(this.hass!);
@ -60,41 +63,57 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}); });
} }
private _applyTheme(dark: boolean) { private _applyTheme(darkPreferred: boolean) {
if (!this.hass) { if (!this.hass) {
return; return;
} }
const themeName = const themeName =
this.hass.selectedTheme?.theme || this.hass.selectedThemeSettings?.theme ||
(dark && this.hass.themes.default_dark_theme (darkPreferred && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme! ? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
let options: Partial<HomeAssistant["selectedTheme"]> = this.hass! let themeSettings: Partial<HomeAssistant["selectedThemeSettings"]> = this
.selectedTheme; .hass!.selectedThemeSettings;
if (themeName === "default" && options?.dark === undefined) { let darkMode =
options = { themeSettings?.dark === undefined ? darkPreferred : themeSettings?.dark;
...this.hass.selectedTheme!,
dark, const selectedTheme =
}; themeSettings?.theme !== undefined
? this.hass.themes.themes[themeSettings.theme]
: undefined;
if (selectedTheme) {
// Override dark mode selection depending on what the theme actually provides.
// Leave the selection as-is if the theme supports the requested mode.
if (darkMode && !selectedTheme.modes?.dark) {
darkMode = false;
} else if (
!darkMode &&
!selectedTheme.modes?.light &&
selectedTheme.modes?.dark
) {
darkMode = true;
} }
}
themeSettings = { ...this.hass.selectedThemeSettings, dark: darkMode };
applyThemesOnElement( applyThemesOnElement(
document.documentElement, document.documentElement,
this.hass.themes, this.hass.themes,
themeName, themeName,
options themeSettings
); );
const darkMode = // Now determine value that should be stored in the local storage settings
themeName === "default" darkMode =
? !!options?.dark darkMode || !!(darkPreferred && this.hass.themes.default_dark_theme);
: !!(dark && this.hass.themes.default_dark_theme);
if (darkMode !== this.hass.themes.darkMode) { if (darkMode !== this.hass.themes.darkMode) {
this._updateHass({ this._updateHass({
themes: { ...this.hass.themes, darkMode }, themes: { ...this.hass.themes!, darkMode },
}); });
const schemeMeta = document.querySelector("meta[name=color-scheme]"); const schemeMeta = document.querySelector("meta[name=color-scheme]");

View File

@ -82,8 +82,12 @@ export interface CurrentUser {
mfa_modules: MFAModule[]; mfa_modules: MFAModule[];
} }
// Currently selected theme and its settings. These are the values stored in local storage.
export interface ThemeSettings { export interface ThemeSettings {
theme: string; theme: string;
// Radio box selection for theme picker. Do not use in cards as
// it can be undefined == auto.
// Property hass.themes.darkMode carries effective current mode.
dark?: boolean; dark?: boolean;
primaryColor?: string; primaryColor?: string;
accentColor?: string; accentColor?: string;
@ -190,7 +194,7 @@ export interface HomeAssistant {
services: HassServices; services: HassServices;
config: HassConfig; config: HassConfig;
themes: Themes; themes: Themes;
selectedTheme?: ThemeSettings | null; selectedThemeSettings: ThemeSettings | null;
panels: Panels; panels: Panels;
panelUrl: string; panelUrl: string;
// i18n // i18n

View File

@ -2,13 +2,16 @@ import { HomeAssistant } from "../types";
const STORED_STATE = [ const STORED_STATE = [
"dockedSidebar", "dockedSidebar",
"selectedTheme", "selectedThemeSettings",
"selectedLanguage", "selectedLanguage",
"vibrate", "vibrate",
"suspendWhenHidden", "suspendWhenHidden",
"enableShortcuts", "enableShortcuts",
"defaultPanel", "defaultPanel",
]; ];
// Deprecated states will be loaded once so that the values can be migrated to other states if required,
// but during the next state storing, the deprecated keys will be removed.
const STORED_STATE_DEPRECATED = ["selectedTheme"];
const STORAGE = window.localStorage || {}; const STORAGE = window.localStorage || {};
export function storeState(hass: HomeAssistant) { export function storeState(hass: HomeAssistant) {
@ -17,6 +20,9 @@ export function storeState(hass: HomeAssistant) {
const value = hass[key]; const value = hass[key];
STORAGE[key] = JSON.stringify(value === undefined ? null : value); STORAGE[key] = JSON.stringify(value === undefined ? null : value);
}); });
STORED_STATE_DEPRECATED.forEach((key) => {
if (key in STORAGE) delete STORAGE[key];
});
} catch (err) { } catch (err) {
// Safari throws exception in private mode // Safari throws exception in private mode
} }
@ -25,13 +31,17 @@ export function storeState(hass: HomeAssistant) {
export function getState() { export function getState() {
const state = {}; const state = {};
STORED_STATE.forEach((key) => { STORED_STATE.concat(STORED_STATE_DEPRECATED).forEach((key) => {
if (key in STORAGE) { if (key in STORAGE) {
let value = JSON.parse(STORAGE[key]); let value = JSON.parse(STORAGE[key]);
// selectedTheme went from string to object on 20200718 // selectedTheme went from string to object on 20200718
if (key === "selectedTheme" && typeof value === "string") { if (key === "selectedTheme" && typeof value === "string") {
value = { theme: value }; value = { theme: value };
} }
// selectedTheme was renamed to selectedThemeSettings on 20210207
if (key === "selectedTheme") {
key = "selectedThemeSettings";
}
// dockedSidebar went from boolean to enum on 20190720 // dockedSidebar went from boolean to enum on 20190720
if (key === "dockedSidebar" && typeof value === "boolean") { if (key === "dockedSidebar" && typeof value === "boolean") {
value = value ? "docked" : "auto"; value = value ? "docked" : "auto";