mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 00:36:34 +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() {
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,62 +23,90 @@ 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) {
|
|
||||||
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 (primaryColor) {
|
||||||
if (element._themes?.cacheKey === cacheKey) {
|
cacheKey = `${cacheKey}__primary_${primaryColor}`;
|
||||||
return;
|
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]) {
|
// 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,
|
||||||
};
|
};
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
<paper-input
|
? html` <div class="color-pickers">
|
||||||
.value=${this.hass!.selectedTheme?.primaryColor || "#03a9f4"}
|
<paper-input
|
||||||
type="color"
|
.value=${themeSettings?.primaryColor ||
|
||||||
.label=${this.hass!.localize(
|
DEFAULT_PRIMARY_COLOR}
|
||||||
"ui.panel.profile.themes.primary_color"
|
type="color"
|
||||||
)}
|
.label=${this.hass.localize(
|
||||||
.name=${"primaryColor"}
|
"ui.panel.profile.themes.primary_color"
|
||||||
@change=${this._handleColorChange}
|
)}
|
||||||
></paper-input>
|
.name=${"primaryColor"}
|
||||||
<paper-input
|
@change=${this._handleColorChange}
|
||||||
.value=${this.hass!.selectedTheme?.accentColor || "#ff9800"}
|
></paper-input>
|
||||||
type="color"
|
<paper-input
|
||||||
.label=${this.hass!.localize(
|
.value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
|
||||||
"ui.panel.profile.themes.accent_color"
|
type="color"
|
||||||
)}
|
.label=${this.hass.localize(
|
||||||
.name=${"accentColor"}
|
"ui.panel.profile.themes.accent_color"
|
||||||
@change=${this._handleColorChange}
|
)}
|
||||||
></paper-input>
|
.name=${"accentColor"}
|
||||||
${this.hass!.selectedTheme?.primaryColor ||
|
@change=${this._handleColorChange}
|
||||||
this.hass!.selectedTheme?.accentColor
|
></paper-input>
|
||||||
? html` <mwc-button @click=${this._resetColors}>
|
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||||
${this.hass!.localize("ui.panel.profile.themes.reset")}
|
? html` <mwc-button @click=${this._resetColors}>
|
||||||
</mwc-button>`
|
${this.hass.localize("ui.panel.profile.themes.reset")}
|
||||||
: ""}
|
</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 {
|
||||||
|
@ -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;");
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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]");
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user