mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Add support for custom themes to use dark mode (#8347)
This commit is contained in:
parent
8af05e2726
commit
7f75ca81f1
@ -103,25 +103,27 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
let themeSettings:
|
||||
| Partial<HomeAssistant["selectedThemeSettings"]>
|
||||
| undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
this.hass.selectedThemeSettings?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
themeSettings = this.hass.selectedThemeSettings;
|
||||
if (themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...this.hass.selectedThemeSettings,
|
||||
dark: this.hass.themes.darkMode,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
themeName =
|
||||
((this.hass.selectedTheme as unknown) as string) ||
|
||||
((this.hass.selectedThemeSettings as unknown) as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
@ -129,7 +131,7 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
themeSettings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Theme } from "../../data/ws-themes";
|
||||
import { ThemeVars } from "../../data/ws-themes";
|
||||
import { darkStyles, derivedStyles } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@ -23,62 +23,90 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS Theme information
|
||||
* selectedTheme: selected theme.
|
||||
* themes: HASS theme information.
|
||||
* selectedTheme: Selected theme.
|
||||
* themeSettings: Settings such as selected dark mode and colors.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string,
|
||||
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||
themeSettings?: Partial<HomeAssistant["selectedThemeSettings"]>
|
||||
) => {
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<Theme> = {};
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
|
||||
if (selectedTheme === "default" && themeOptions) {
|
||||
if (themeOptions.dark) {
|
||||
if (themeSettings) {
|
||||
if (themeSettings.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
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(
|
||||
themeOptions.primaryColor,
|
||||
primaryColor,
|
||||
"#121212",
|
||||
8
|
||||
);
|
||||
}
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (themeOptions.accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||
themeRules["accent-color"] = themeOptions.accentColor;
|
||||
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
if (primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = primaryColor;
|
||||
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${accentColor}`;
|
||||
themeRules["accent-color"] = accentColor;
|
||||
const rgbAccentColor = hex2rgb(accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
// Custom theme logic (not relevant for default theme, since it would override
|
||||
// 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) {
|
||||
@ -106,12 +134,12 @@ export const applyThemesOnElement = (
|
||||
|
||||
const processTheme = (
|
||||
cacheKey: string,
|
||||
theme: Partial<Theme>
|
||||
theme: Partial<ThemeVars>
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!theme || !Object.keys(theme).length) {
|
||||
return undefined;
|
||||
}
|
||||
const combinedTheme: Partial<Theme> = {
|
||||
const combinedTheme: Partial<ThemeVars> = {
|
||||
...derivedStyles,
|
||||
...theme,
|
||||
};
|
||||
|
@ -108,7 +108,7 @@ class LocationEditor extends LitElement {
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
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;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
@ -118,7 +118,7 @@ class LocationEditor extends LitElement {
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes?.darkMode
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -130,7 +130,7 @@ class LocationEditor extends LitElement {
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
this.darkMode ?? this.hass.themes?.darkMode,
|
||||
this.darkMode ?? this.hass.themes.darkMode,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
|
||||
export interface Theme {
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
@ -8,10 +8,20 @@ export interface Theme {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
light?: ThemeVars;
|
||||
dark?: ThemeVars;
|
||||
};
|
||||
};
|
||||
|
||||
export interface Themes {
|
||||
default_theme: string;
|
||||
default_dark_theme: string | null;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { navigate } from "../common/navigate";
|
||||
import {
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
DEFAULT_PRIMARY_COLOR,
|
||||
} from "../resources/ha-style";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const defaultRadiusColor = "#FF9800";
|
||||
export const homeRadiusColor = "#03a9f4";
|
||||
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
|
||||
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
|
||||
export const passiveRadiusColor = "#9b9b9b";
|
||||
|
||||
export interface Zone {
|
||||
|
@ -277,7 +277,7 @@ export const provideHass = (
|
||||
mockTheme(theme) {
|
||||
invalidateThemeCache();
|
||||
hass().updateHass({
|
||||
selectedTheme: { theme: theme ? "mock" : "default" },
|
||||
selectedThemeSettings: { theme: theme ? "mock" : "default" },
|
||||
themes: {
|
||||
...hass().themes,
|
||||
themes: {
|
||||
@ -285,11 +285,11 @@ export const provideHass = (
|
||||
},
|
||||
},
|
||||
});
|
||||
const { themes, selectedTheme } = hass();
|
||||
const { themes, selectedThemeSettings } = hass();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme!.theme
|
||||
selectedThemeSettings!.theme
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -81,25 +81,27 @@ class SupervisorErrorScreen extends LitElement {
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
let themeSettings:
|
||||
| Partial<HomeAssistant["selectedThemeSettings"]>
|
||||
| undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
this.hass.selectedThemeSettings?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
themeSettings = this.hass.selectedThemeSettings;
|
||||
if (themeName === "default" && themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...this.hass.selectedThemeSettings,
|
||||
dark: this.hass.themes.darkMode,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
themeName =
|
||||
((this.hass.selectedTheme as unknown) as string) ||
|
||||
((this.hass.selectedThemeSettings as unknown) as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
@ -107,7 +109,7 @@ class SupervisorErrorScreen extends LitElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
themeSettings
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,7 @@ export class HUIView extends ReactiveElement {
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass ||
|
||||
this.hass.themes !== oldHass.themes ||
|
||||
this.hass.selectedTheme !== oldHass.selectedTheme)
|
||||
this.hass.selectedThemeSettings !== oldHass.selectedThemeSettings)
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
|
||||
}
|
||||
|
@ -17,6 +17,11 @@ import "../../components/ha-paper-dropdown-menu";
|
||||
import "../../components/ha-radio";
|
||||
import type { HaRadio } from "../../components/ha-radio";
|
||||
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 { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
@ -26,15 +31,20 @@ export class HaPickThemeRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() _themes: string[] = [];
|
||||
@state() _themeNames: string[] = [];
|
||||
|
||||
@state() _selectedTheme = 0;
|
||||
@state() _selectedThemeIndex = 0;
|
||||
|
||||
@state() _selectedTheme?: Theme;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
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 =
|
||||
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`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
@ -46,7 +56,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
: ""}
|
||||
<a
|
||||
href="${documentationUrl(
|
||||
this.hass!,
|
||||
this.hass,
|
||||
"/integrations/frontend/#defining-themes"
|
||||
)}"
|
||||
target="_blank"
|
||||
@ -62,19 +72,20 @@ export class HaPickThemeRow extends LitElement {
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._selectedTheme}
|
||||
.selected=${this._selectedThemeIndex}
|
||||
@iron-select=${this._handleThemeSelection}
|
||||
>
|
||||
${this._themes.map(
|
||||
${this._themeNames.map(
|
||||
(theme) => html`<paper-item .theme=${theme}>${theme}</paper-item>`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</ha-settings-row>
|
||||
${curTheme === "default"
|
||||
${curTheme === "default" ||
|
||||
(this._selectedTheme && this._supportsModeSelection(this._selectedTheme))
|
||||
? html` <div class="inputs">
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.auto"
|
||||
)}
|
||||
>
|
||||
@ -82,11 +93,11 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="auto"
|
||||
?checked=${this.hass.selectedTheme?.dark === undefined}
|
||||
?checked=${themeSettings?.dark === undefined}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.light"
|
||||
)}
|
||||
>
|
||||
@ -94,12 +105,12 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="light"
|
||||
?checked=${this.hass.selectedTheme?.dark === false}
|
||||
?checked=${themeSettings?.dark === false}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.dark"
|
||||
)}
|
||||
>
|
||||
@ -107,36 +118,38 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="dark"
|
||||
?checked=${this.hass.selectedTheme?.dark === true}
|
||||
?checked=${themeSettings?.dark === true}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<div class="color-pickers">
|
||||
<paper-input
|
||||
.value=${this.hass!.selectedTheme?.primaryColor || "#03a9f4"}
|
||||
type="color"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.profile.themes.primary_color"
|
||||
)}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.value=${this.hass!.selectedTheme?.accentColor || "#ff9800"}
|
||||
type="color"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
)}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></paper-input>
|
||||
${this.hass!.selectedTheme?.primaryColor ||
|
||||
this.hass!.selectedTheme?.accentColor
|
||||
? html` <mwc-button @click=${this._resetColors}>
|
||||
${this.hass!.localize("ui.panel.profile.themes.reset")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
${curTheme === "default"
|
||||
? html` <div class="color-pickers">
|
||||
<paper-input
|
||||
.value=${themeSettings?.primaryColor ||
|
||||
DEFAULT_PRIMARY_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.primary_color"
|
||||
)}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
)}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></paper-input>
|
||||
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html` <mwc-button @click=${this._resetColors}>
|
||||
${this.hass.localize("ui.panel.profile.themes.reset")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
`;
|
||||
@ -146,27 +159,31 @@ export class HaPickThemeRow extends LitElement {
|
||||
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
|
||||
const themesChanged =
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.themes?.themes !== this.hass.themes?.themes);
|
||||
(!oldHass || oldHass.themes.themes !== this.hass.themes.themes);
|
||||
const selectedThemeChanged =
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.selectedTheme !== this.hass.selectedTheme);
|
||||
(!oldHass ||
|
||||
oldHass.selectedThemeSettings !== this.hass.selectedThemeSettings);
|
||||
|
||||
if (themesChanged) {
|
||||
this._themes = ["Backend-selected", "default"].concat(
|
||||
this._themeNames = ["Backend-selected", "default"].concat(
|
||||
Object.keys(this.hass.themes.themes).sort()
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedThemeChanged) {
|
||||
if (
|
||||
this.hass.selectedTheme &&
|
||||
this._themes.indexOf(this.hass.selectedTheme.theme) > 0
|
||||
this.hass.selectedThemeSettings &&
|
||||
this._themeNames.indexOf(this.hass.selectedThemeSettings.theme) > 0
|
||||
) {
|
||||
this._selectedTheme = this._themes.indexOf(
|
||||
this.hass.selectedTheme.theme
|
||||
this._selectedThemeIndex = this._themeNames.indexOf(
|
||||
this.hass.selectedThemeSettings.theme
|
||||
);
|
||||
} else if (!this.hass.selectedTheme) {
|
||||
this._selectedTheme = 0;
|
||||
this._selectedTheme = this.hass.themes.themes[
|
||||
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) {
|
||||
let dark: boolean | undefined;
|
||||
switch ((ev.target as HaRadio).value) {
|
||||
@ -199,12 +220,20 @@ export class HaPickThemeRow extends LitElement {
|
||||
private _handleThemeSelection(ev: CustomEvent) {
|
||||
const theme = ev.detail.item.theme;
|
||||
if (theme === "Backend-selected") {
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "settheme", { theme: "" });
|
||||
if (this.hass.selectedThemeSettings?.theme) {
|
||||
fireEvent(this, "settheme", {
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "settheme", { theme });
|
||||
fireEvent(this, "settheme", {
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -2,6 +2,9 @@ import "@polymer/paper-styles/paper-styles";
|
||||
import "@polymer/polymer/lib/elements/custom-style";
|
||||
import { derivedStyles } from "./styles";
|
||||
|
||||
export const DEFAULT_PRIMARY_COLOR = "#03a9f4";
|
||||
export const DEFAULT_ACCENT_COLOR = "#ff9800";
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
|
||||
|
@ -39,6 +39,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
states: null as any,
|
||||
config: null as any,
|
||||
themes: null as any,
|
||||
selectedThemeSettings: null,
|
||||
panels: null as any,
|
||||
services: null as any,
|
||||
user: null as any,
|
||||
|
@ -11,10 +11,10 @@ import { HassBaseEl } from "./hass-base-mixin";
|
||||
declare global {
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
settheme: HASSDomEvent<Partial<HomeAssistant["selectedTheme"]>>;
|
||||
settheme: HASSDomEvent<Partial<HomeAssistant["selectedThemeSettings"]>>;
|
||||
}
|
||||
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);
|
||||
this.addEventListener("settheme", (ev) => {
|
||||
this._updateHass({
|
||||
selectedTheme: { ...this.hass!.selectedTheme!, ...ev.detail },
|
||||
selectedThemeSettings: {
|
||||
...this.hass!.selectedThemeSettings!,
|
||||
...ev.detail,
|
||||
},
|
||||
});
|
||||
this._applyTheme(mql.matches);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
(dark && this.hass.themes.default_dark_theme
|
||||
this.hass.selectedThemeSettings?.theme ||
|
||||
(darkPreferred && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
let options: Partial<HomeAssistant["selectedTheme"]> = this.hass!
|
||||
.selectedTheme;
|
||||
let themeSettings: Partial<HomeAssistant["selectedThemeSettings"]> = this
|
||||
.hass!.selectedThemeSettings;
|
||||
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme!,
|
||||
dark,
|
||||
};
|
||||
let darkMode =
|
||||
themeSettings?.dark === undefined ? darkPreferred : themeSettings?.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(
|
||||
document.documentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
themeSettings
|
||||
);
|
||||
|
||||
const darkMode =
|
||||
themeName === "default"
|
||||
? !!options?.dark
|
||||
: !!(dark && this.hass.themes.default_dark_theme);
|
||||
// Now determine value that should be stored in the local storage settings
|
||||
darkMode =
|
||||
darkMode || !!(darkPreferred && this.hass.themes.default_dark_theme);
|
||||
|
||||
if (darkMode !== this.hass.themes.darkMode) {
|
||||
this._updateHass({
|
||||
themes: { ...this.hass.themes, darkMode },
|
||||
themes: { ...this.hass.themes!, darkMode },
|
||||
});
|
||||
|
||||
const schemeMeta = document.querySelector("meta[name=color-scheme]");
|
||||
|
@ -82,8 +82,12 @@ export interface CurrentUser {
|
||||
mfa_modules: MFAModule[];
|
||||
}
|
||||
|
||||
// Currently selected theme and its settings. These are the values stored in local storage.
|
||||
export interface ThemeSettings {
|
||||
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;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
@ -190,7 +194,7 @@ export interface HomeAssistant {
|
||||
services: HassServices;
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme?: ThemeSettings | null;
|
||||
selectedThemeSettings: ThemeSettings | null;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
// i18n
|
||||
|
@ -2,13 +2,16 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
const STORED_STATE = [
|
||||
"dockedSidebar",
|
||||
"selectedTheme",
|
||||
"selectedThemeSettings",
|
||||
"selectedLanguage",
|
||||
"vibrate",
|
||||
"suspendWhenHidden",
|
||||
"enableShortcuts",
|
||||
"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 || {};
|
||||
|
||||
export function storeState(hass: HomeAssistant) {
|
||||
@ -17,6 +20,9 @@ export function storeState(hass: HomeAssistant) {
|
||||
const value = hass[key];
|
||||
STORAGE[key] = JSON.stringify(value === undefined ? null : value);
|
||||
});
|
||||
STORED_STATE_DEPRECATED.forEach((key) => {
|
||||
if (key in STORAGE) delete STORAGE[key];
|
||||
});
|
||||
} catch (err) {
|
||||
// Safari throws exception in private mode
|
||||
}
|
||||
@ -25,13 +31,17 @@ export function storeState(hass: HomeAssistant) {
|
||||
export function getState() {
|
||||
const state = {};
|
||||
|
||||
STORED_STATE.forEach((key) => {
|
||||
STORED_STATE.concat(STORED_STATE_DEPRECATED).forEach((key) => {
|
||||
if (key in STORAGE) {
|
||||
let value = JSON.parse(STORAGE[key]);
|
||||
// selectedTheme went from string to object on 20200718
|
||||
if (key === "selectedTheme" && typeof value === "string") {
|
||||
value = { theme: value };
|
||||
}
|
||||
// selectedTheme was renamed to selectedThemeSettings on 20210207
|
||||
if (key === "selectedTheme") {
|
||||
key = "selectedThemeSettings";
|
||||
}
|
||||
// dockedSidebar went from boolean to enum on 20190720
|
||||
if (key === "dockedSidebar" && typeof value === "boolean") {
|
||||
value = value ? "docked" : "auto";
|
||||
|
Loading…
x
Reference in New Issue
Block a user