mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-19 09:40:14 +00:00
feat: synchronize background image with active light/dark theme
This commit is contained in:
+27
-17
@@ -1,5 +1,4 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { DateTime } from "luxon";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -40,16 +39,16 @@ const MainApp: React.FC = () => {
|
|||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme, effectiveTheme } = useTheme();
|
||||||
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
|
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
|
||||||
const { backgroundImage: customBackgroundImage } = useBackground();
|
const { backgroundImage: customBackgroundImage, updateBackground } =
|
||||||
|
useBackground();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServerDetail();
|
loadServerDetail();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateConfig = () => {
|
|
||||||
const config = settingData?.data?.config;
|
const config = settingData?.data?.config;
|
||||||
if (config) {
|
if (config) {
|
||||||
if (config.custom_code) {
|
if (config.custom_code) {
|
||||||
@@ -63,23 +62,34 @@ const MainApp: React.FC = () => {
|
|||||||
window.CustomDesc = config.custom_description;
|
window.CustomDesc = config.custom_description;
|
||||||
if (config.custom_links) window.CustomLinks = config.custom_links;
|
if (config.custom_links) window.CustomLinks = config.custom_links;
|
||||||
|
|
||||||
const hour = DateTime.now().hour;
|
if (config.background_image_day) {
|
||||||
const isNight = hour >= 18 || hour < 6;
|
window.CustomBackgroundImageDay = config.background_image_day;
|
||||||
|
}
|
||||||
if (isNight && config.background_image_night) {
|
if (config.background_image_night) {
|
||||||
window.CustomBackgroundImage = config.background_image_night;
|
window.CustomBackgroundImageNight = config.background_image_night;
|
||||||
} else if (!isNight && config.background_image_day) {
|
|
||||||
window.CustomBackgroundImage = config.background_image_day;
|
|
||||||
}
|
}
|
||||||
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
updateConfig();
|
|
||||||
const interval = setInterval(updateConfig, 60000); // Check every minute
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [settingData]);
|
}, [settingData]);
|
||||||
|
|
||||||
|
// 监听有效主题以及配置变化,动态切换日夜背景
|
||||||
|
useEffect(() => {
|
||||||
|
const config = settingData?.data?.config;
|
||||||
|
const bgDay =
|
||||||
|
config?.background_image_day || window.CustomBackgroundImageDay;
|
||||||
|
const bgNight =
|
||||||
|
config?.background_image_night || window.CustomBackgroundImageNight;
|
||||||
|
|
||||||
|
const targetBg =
|
||||||
|
effectiveTheme === "dark"
|
||||||
|
? bgNight || window.CustomBackgroundImage
|
||||||
|
: bgDay || window.CustomBackgroundImage;
|
||||||
|
|
||||||
|
if (targetBg) {
|
||||||
|
updateBackground?.(targetBg);
|
||||||
|
window.CustomMobileBackgroundImage = targetBg;
|
||||||
|
}
|
||||||
|
}, [effectiveTheme, settingData, updateBackground]);
|
||||||
|
|
||||||
// 检测是否强制指定了主题颜色
|
// 检测是否强制指定了主题颜色
|
||||||
const forceTheme =
|
const forceTheme =
|
||||||
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ type ThemeProviderProps = {
|
|||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
|
effectiveTheme: "light" | "dark";
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
setTheme: () => null,
|
setTheme: () => null,
|
||||||
|
effectiveTheme: "light",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
@@ -58,6 +60,17 @@ export function ThemeProvider({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const effectiveTheme: "light" | "dark" = useMemo(() => {
|
||||||
|
if (theme === "system") {
|
||||||
|
return isSystemDark ? "dark" : "light";
|
||||||
|
}
|
||||||
|
if (theme === "scheduled") {
|
||||||
|
const isNight = hour >= 18 || hour < 6;
|
||||||
|
return isNight ? "dark" : "light";
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
}, [theme, isSystemDark, hour]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement;
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
@@ -79,17 +92,6 @@ export function ThemeProvider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
root.classList.add("disable-transitions");
|
root.classList.add("disable-transitions");
|
||||||
|
|
||||||
let effectiveTheme: "light" | "dark" = "light";
|
|
||||||
if (theme === "system") {
|
|
||||||
effectiveTheme = isSystemDark ? "dark" : "light";
|
|
||||||
} else if (theme === "scheduled") {
|
|
||||||
const isNight = hour >= 18 || hour < 6;
|
|
||||||
effectiveTheme = isNight ? "dark" : "light";
|
|
||||||
} else {
|
|
||||||
effectiveTheme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme(effectiveTheme);
|
applyTheme(effectiveTheme);
|
||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
@@ -99,7 +101,7 @@ export function ThemeProvider({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [theme, hour, isSystemDark]);
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
const handleSetTheme = useCallback(
|
const handleSetTheme = useCallback(
|
||||||
(nextTheme: Theme) => {
|
(nextTheme: Theme) => {
|
||||||
@@ -113,8 +115,9 @@ export function ThemeProvider({
|
|||||||
() => ({
|
() => ({
|
||||||
theme,
|
theme,
|
||||||
setTheme: handleSetTheme,
|
setTheme: handleSetTheme,
|
||||||
|
effectiveTheme,
|
||||||
}),
|
}),
|
||||||
[theme, handleSetTheme],
|
[theme, handleSetTheme, effectiveTheme],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CustomBackgroundImage: string;
|
CustomBackgroundImage: string;
|
||||||
|
CustomBackgroundImageDay?: string;
|
||||||
|
CustomBackgroundImageNight?: string;
|
||||||
CustomMobileBackgroundImage: string;
|
CustomMobileBackgroundImage: string;
|
||||||
ForceShowServices: boolean;
|
ForceShowServices: boolean;
|
||||||
ForceCardInline: boolean;
|
ForceCardInline: boolean;
|
||||||
@@ -17,7 +19,7 @@ const BACKGROUND_CHANGE_EVENT = "backgroundChange";
|
|||||||
|
|
||||||
export function useBackground() {
|
export function useBackground() {
|
||||||
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
|
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
|
||||||
undefined,
|
() => window.CustomBackgroundImage || undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,10 +63,10 @@ export function useBackground() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateBackground = (newBackground: string | undefined) => {
|
const updateBackground = useCallback((newBackground: string | undefined) => {
|
||||||
window.CustomBackgroundImage = newBackground || "";
|
window.CustomBackgroundImage = newBackground || "";
|
||||||
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
|
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return { backgroundImage, updateBackground };
|
return { backgroundImage, updateBackground };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,38 @@ import { DateTime } from "luxon";
|
|||||||
|
|
||||||
export function initCustomConfig() {
|
export function initCustomConfig() {
|
||||||
try {
|
try {
|
||||||
|
// Default day / night background images
|
||||||
|
window.CustomBackgroundImageDay =
|
||||||
|
window.CustomBackgroundImageDay ||
|
||||||
|
"https://loohui.com/wp-content/uploads/images/background_day.jpg";
|
||||||
|
window.CustomBackgroundImageNight =
|
||||||
|
window.CustomBackgroundImageNight ||
|
||||||
|
"https://loohui.com/wp-content/uploads/images/background.jpg";
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem("vite-ui-theme");
|
||||||
|
const systemDark =
|
||||||
|
typeof window !== "undefined" && window.matchMedia
|
||||||
|
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
: false;
|
||||||
const hour = DateTime.now().hour;
|
const hour = DateTime.now().hour;
|
||||||
const isNight = hour >= 18 || hour < 6;
|
const isScheduledNight = hour >= 18 || hour < 6;
|
||||||
|
|
||||||
// Use default values if window variables are not already set (e.g. by backend custom_code)
|
let isDark = false;
|
||||||
// although the goal is to hardcode these for "consistency".
|
if (savedTheme === "dark") {
|
||||||
|
isDark = true;
|
||||||
|
} else if (savedTheme === "light") {
|
||||||
|
isDark = false;
|
||||||
|
} else if (savedTheme === "scheduled") {
|
||||||
|
isDark = isScheduledNight;
|
||||||
|
} else if (savedTheme === "system") {
|
||||||
|
isDark = systemDark;
|
||||||
|
} else {
|
||||||
|
isDark = isScheduledNight;
|
||||||
|
}
|
||||||
|
|
||||||
window.CustomBackgroundImage = isNight
|
window.CustomBackgroundImage = isDark
|
||||||
? "https://loohui.com/wp-content/uploads/images/background.jpg"
|
? window.CustomBackgroundImageNight
|
||||||
: "https://loohui.com/wp-content/uploads/images/background_day.jpg";
|
: window.CustomBackgroundImageDay;
|
||||||
|
|
||||||
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
||||||
|
|
||||||
|
|||||||
+65
-2
@@ -9,6 +9,8 @@ const appMocks = vi.hoisted(() => ({
|
|||||||
fetchSetting: vi.fn(),
|
fetchSetting: vi.fn(),
|
||||||
injectContext: vi.fn(),
|
injectContext: vi.fn(),
|
||||||
setTheme: vi.fn(),
|
setTheme: vi.fn(),
|
||||||
|
updateBackground: vi.fn(),
|
||||||
|
effectiveTheme: "light" as "light" | "dark",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../components/DashCommand", () => ({
|
vi.mock("../components/DashCommand", () => ({
|
||||||
@@ -38,11 +40,17 @@ vi.mock("../pages/ServerDetail", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../hooks/use-background", () => ({
|
vi.mock("../hooks/use-background", () => ({
|
||||||
useBackground: () => ({ backgroundImage: appMocks.backgroundImage }),
|
useBackground: () => ({
|
||||||
|
backgroundImage: appMocks.backgroundImage,
|
||||||
|
updateBackground: appMocks.updateBackground,
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../hooks/use-theme", () => ({
|
vi.mock("../hooks/use-theme", () => ({
|
||||||
useTheme: () => ({ setTheme: appMocks.setTheme }),
|
useTheme: () => ({
|
||||||
|
setTheme: appMocks.setTheme,
|
||||||
|
effectiveTheme: appMocks.effectiveTheme,
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../lib/inject", () => ({
|
vi.mock("../lib/inject", () => ({
|
||||||
@@ -90,7 +98,11 @@ describe("App", () => {
|
|||||||
appMocks.fetchSetting.mockResolvedValue(settingResponse());
|
appMocks.fetchSetting.mockResolvedValue(settingResponse());
|
||||||
appMocks.injectContext.mockResolvedValue(undefined);
|
appMocks.injectContext.mockResolvedValue(undefined);
|
||||||
appMocks.setTheme.mockClear();
|
appMocks.setTheme.mockClear();
|
||||||
|
appMocks.updateBackground.mockClear();
|
||||||
|
appMocks.effectiveTheme = "light";
|
||||||
window.ForceTheme = "";
|
window.ForceTheme = "";
|
||||||
|
window.CustomBackgroundImageDay = "/day.png";
|
||||||
|
window.CustomBackgroundImageNight = "/night.png";
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,4 +214,55 @@ describe("App", () => {
|
|||||||
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||||
expect(appMocks.setTheme).not.toHaveBeenCalledWith("dark");
|
expect(appMocks.setTheme).not.toHaveBeenCalledWith("dark");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates background to day background when effective theme is light", async () => {
|
||||||
|
appMocks.effectiveTheme = "light";
|
||||||
|
window.CustomBackgroundImageDay = "/day.png";
|
||||||
|
window.CustomBackgroundImageNight = "/night.png";
|
||||||
|
|
||||||
|
renderApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(appMocks.updateBackground).toHaveBeenCalledWith("/day.png");
|
||||||
|
});
|
||||||
|
expect(window.CustomMobileBackgroundImage).toBe("/day.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates background to night background when effective theme is dark", async () => {
|
||||||
|
appMocks.effectiveTheme = "dark";
|
||||||
|
window.CustomBackgroundImageDay = "/day.png";
|
||||||
|
window.CustomBackgroundImageNight = "/night.png";
|
||||||
|
|
||||||
|
renderApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(appMocks.updateBackground).toHaveBeenCalledWith("/night.png");
|
||||||
|
});
|
||||||
|
expect(window.CustomMobileBackgroundImage).toBe("/night.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies day and night background images from backend settings config", async () => {
|
||||||
|
appMocks.effectiveTheme = "light";
|
||||||
|
appMocks.fetchSetting.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
...settingResponse().data.config,
|
||||||
|
background_image_day: "/backend-day.png",
|
||||||
|
background_image_night: "/backend-night.png",
|
||||||
|
},
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(appMocks.updateBackground).toHaveBeenCalledWith(
|
||||||
|
"/backend-day.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(window.CustomBackgroundImageDay).toBe("/backend-day.png");
|
||||||
|
expect(window.CustomBackgroundImageNight).toBe("/backend-night.png");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import {
|
|||||||
import { useTheme } from "@/hooks/use-theme";
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
|
||||||
function ThemeProbe() {
|
function ThemeProbe() {
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme, effectiveTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p data-testid="theme-state">{theme}</p>
|
<p data-testid="theme-state">{theme}</p>
|
||||||
|
<p data-testid="effective-theme-state">{effectiveTheme}</p>
|
||||||
<button type="button" onClick={() => setTheme("dark")}>
|
<button type="button" onClick={() => setTheme("dark")}>
|
||||||
dark
|
dark
|
||||||
</button>
|
</button>
|
||||||
@@ -43,11 +44,17 @@ describe("ThemeProvider", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId("theme-state")).toHaveTextContent("system");
|
expect(screen.getByTestId("theme-state")).toHaveTextContent("system");
|
||||||
|
expect(screen.getByTestId("effective-theme-state")).toHaveTextContent(
|
||||||
|
"light",
|
||||||
|
);
|
||||||
expect(document.documentElement).toHaveClass("light");
|
expect(document.documentElement).toHaveClass("light");
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "dark" }));
|
await user.click(screen.getByRole("button", { name: "dark" }));
|
||||||
|
|
||||||
expect(screen.getByTestId("theme-state")).toHaveTextContent("dark");
|
expect(screen.getByTestId("theme-state")).toHaveTextContent("dark");
|
||||||
|
expect(screen.getByTestId("effective-theme-state")).toHaveTextContent(
|
||||||
|
"dark",
|
||||||
|
);
|
||||||
expect(localStorage.getItem("theme-test")).toBe("dark");
|
expect(localStorage.getItem("theme-test")).toBe("dark");
|
||||||
expect(document.documentElement).toHaveClass("dark");
|
expect(document.documentElement).toHaveClass("dark");
|
||||||
expect(document.documentElement.style.colorScheme).toBe("dark");
|
expect(document.documentElement.style.colorScheme).toBe("dark");
|
||||||
@@ -57,6 +64,10 @@ describe("ThemeProvider", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "light" }));
|
await user.click(screen.getByRole("button", { name: "light" }));
|
||||||
|
expect(screen.getByTestId("theme-state")).toHaveTextContent("light");
|
||||||
|
expect(screen.getByTestId("effective-theme-state")).toHaveTextContent(
|
||||||
|
"light",
|
||||||
|
);
|
||||||
expect(document.documentElement).toHaveClass("light");
|
expect(document.documentElement).toHaveClass("light");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
CustomBackgroundImage: string;
|
CustomBackgroundImage: string;
|
||||||
|
CustomBackgroundImageDay?: string;
|
||||||
|
CustomBackgroundImageNight?: string;
|
||||||
CustomMobileBackgroundImage: string;
|
CustomMobileBackgroundImage: string;
|
||||||
ForceTheme: string;
|
ForceTheme: string;
|
||||||
CustomLogo: string;
|
CustomLogo: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user