UI Customize: add custom-theme flag for default theme (#33042)

This commit is contained in:
Michael Geers 2026-08-21 14:00:50 +02:00 • committed by GitHub
parent eeb2f593bf
commit a8c4dfd372
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 50 additions and 1 deletions

View file

@ -9,7 +9,7 @@ export function getThemePreference(): THEME | null {
if (theme && Object.values(THEME).includes(theme)) {
return theme;
}
return THEME.AUTO;
return window.evcc?.customTheme || THEME.AUTO;
}
export function setThemePreference(theme: THEME) {

View file

@ -14,6 +14,7 @@ declare global {
customWebsite: string;
customEmail: string;
customPhone: string;
customTheme: THEME;
};
}
interface Window {

View file

@ -46,6 +46,9 @@ const (
flagCustomPhone = "custom-phone"
flagCustomPhoneDescription = "Support phone number shown in the UI"
flagCustomTheme = "custom-theme"
flagCustomThemeDescription = "Default UI theme (auto, light, dark). Users can override in the UI."
flagBatteryMode = "battery-mode"
flagBatteryModeDescription = "Set battery mode (normal, hold, charge, holdcharge)"
flagBatteryModeWait = "battery-mode-wait"

View file

@ -113,6 +113,7 @@ func init() {
rootCmd.Flags().StringVar(&customization.Website, flagCustomWebsite, os.Getenv("EVCC_CUSTOM_WEBSITE"), flagCustomWebsiteDescription)
rootCmd.Flags().StringVar(&customization.Email, flagCustomEmail, os.Getenv("EVCC_CUSTOM_EMAIL"), flagCustomEmailDescription)
rootCmd.Flags().StringVar(&customization.Phone, flagCustomPhone, os.Getenv("EVCC_CUSTOM_PHONE"), flagCustomPhoneDescription)
rootCmd.Flags().StringVar(&customization.Theme, flagCustomTheme, os.Getenv("EVCC_CUSTOM_THEME"), flagCustomThemeDescription)
}
// initConfig reads in config file and ENV variables if set

View file

@ -55,6 +55,7 @@ type Customization struct {
Website string
Email string
Phone string
Theme string
}
// NewHTTPd creates HTTP server with configured routes for loadpoint
@ -112,6 +113,12 @@ func NewHTTPd(addr string, hub *SocketHub, custom Customization) *HTTPd {
log.FATAL.Fatal("custom logo requires both light and dark variants")
}
switch custom.Theme {
case "", "auto", "light", "dark":
default:
log.FATAL.Fatalf("invalid custom theme: %s (expected auto, light, dark)", custom.Theme)
}
for path, file := range map[string]string{
"/custom-logo-light": custom.LogoLight,
"/custom-logo-dark": custom.LogoDark,

View file

@ -59,6 +59,7 @@ func globalsJsHandler(custom Customization) http.HandlerFunc {
Website string `json:"customWebsite"`
Email string `json:"customEmail"`
Phone string `json:"customPhone"`
Theme string `json:"customTheme"`
}{
Version: util.Version,
CustomCss: custom.Css != "",
@ -67,6 +68,7 @@ func globalsJsHandler(custom Customization) http.HandlerFunc {
Website: custom.Website,
Email: custom.Email,
Phone: custom.Phone,
Theme: custom.Theme,
}
return func(w http.ResponseWriter, r *http.Request) {

35
tests/theme.spec.ts Normal file
View file

@ -0,0 +1,35 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { openMoreMenu, expectModalVisible } from "./utils";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start("basics.evcc.yaml", null, ["--disable-auth", "--custom-theme", "dark"]);
});
test.afterAll(async () => {
await stop();
});
test.describe("custom theme", async () => {
test("starts dark, user choice wins and survives reload", async ({ page }) => {
await page.goto("/");
await expect(page.locator("html")).toHaveAttribute("data-bs-theme", "dark");
const menu = await openMoreMenu(page);
await menu.getByRole("button", { name: "User Interface" }).click();
const modal = page.getByTestId("global-settings-modal");
await expectModalVisible(modal);
await expect(modal.getByRole("radio", { name: "dark" })).toHaveAttribute(
"aria-checked",
"true"
);
await modal.getByRole("radio", { name: "light" }).click();
await expect(page.locator("html")).toHaveAttribute("data-bs-theme", "light");
await page.reload();
await expect(page.locator("html")).toHaveAttribute("data-bs-theme", "light");
});
});