fixed dark mode

This commit is contained in:
coolcoala
2025-07-09 04:43:16 +03:00
parent 7cc893383e
commit ec99e24ca1
7 changed files with 143 additions and 316 deletions

View File

@@ -1,12 +1,14 @@
import { AppDataProvider } from "./providers/app-data-provider";
import { ThemeProvider } from "@/components/layout/theme-provider";
import Layout from "./pages/_layout";
function App() {
return (
<AppDataProvider>
<Layout />
</AppDataProvider>
<ThemeProvider>
<AppDataProvider>
<Layout />
</AppDataProvider>
</ThemeProvider>
);
}
export default App;

View File

@@ -0,0 +1,105 @@
import { useEffect } from "react";
import { useVerge } from "@/hooks/use-verge";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Theme as TauriTheme } from "@tauri-apps/api/window";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
type ThemeProviderProps = {
children: React.ReactNode;
};
function hexToHsl(hex?: string): string | undefined {
if (!hex) return undefined;
let r = 0, g = 0, b = 0;
hex = hex.replace("#", "");
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
r = parseInt(hex.substring(0, 2), 16) / 255;
g = parseInt(hex.substring(2, 4), 16) / 255;
b = parseInt(hex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return `${(h * 360).toFixed(1)} ${s.toFixed(3)} ${l.toFixed(3)}`;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const { verge } = useVerge();
const themeModeSetting = verge?.theme_mode || "system";
const customThemeSettings = verge?.theme_setting || {};
useEffect(() => {
const root = window.document.documentElement; // <html> тег
const appWindow = getCurrentWebviewWindow();
const applyTheme = (mode: 'light' | 'dark') => {
root.classList.remove("light", "dark");
root.classList.add(mode);
appWindow.setTheme(mode as TauriTheme).catch(console.error);
const basePalette = mode === 'light' ? defaultTheme : defaultDarkTheme;
const variables = {
"--background": hexToHsl(basePalette.background_color),
"--foreground": hexToHsl(customThemeSettings.primary_text || basePalette.primary_text),
"--card": hexToHsl(basePalette.background_color), // Используем тот же фон
"--card-foreground": hexToHsl(customThemeSettings.primary_text || basePalette.primary_text),
"--popover": hexToHsl(basePalette.background_color),
"--popover-foreground": hexToHsl(customThemeSettings.primary_text || basePalette.primary_text),
"--primary": hexToHsl(customThemeSettings.primary_color || basePalette.primary_color),
"--primary-foreground": hexToHsl("#ffffff"), // Предполагаем белый текст на основном цвете
"--secondary": hexToHsl(customThemeSettings.secondary_color || basePalette.secondary_color),
"--secondary-foreground": hexToHsl(customThemeSettings.primary_text || basePalette.primary_text),
"--muted-foreground": hexToHsl(customThemeSettings.secondary_text || basePalette.secondary_text),
"--destructive": hexToHsl(customThemeSettings.error_color || basePalette.error_color),
"--ring": hexToHsl(customThemeSettings.primary_color || basePalette.primary_color),
};
for (const [key, value] of Object.entries(variables)) {
if(value) root.style.setProperty(key, value);
}
if (customThemeSettings.font_family) {
root.style.setProperty("--font-sans", customThemeSettings.font_family);
} else {
root.style.removeProperty("--font-sans");
}
let styleElement = document.querySelector("style#verge-theme");
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "verge-theme";
document.head.appendChild(styleElement!);
}
if (styleElement) {
styleElement.innerHTML = customThemeSettings.css_injection || "";
}
};
if (themeModeSetting === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
applyTheme(systemTheme);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
if (verge?.theme_mode === 'system') applyTheme(payload);
});
return () => { unlistenPromise.then(f => f()); };
} else {
applyTheme(themeModeSetting);
}
}, [themeModeSetting, customThemeSettings, verge?.theme_mode]);
return <>{children}</>;
}

View File

@@ -1,213 +1,41 @@
import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
import {
zhCN as zhXDataGrid,
enUS as enXDataGrid,
ruRU as ruXDataGrid,
faIR as faXDataGrid,
arSD as arXDataGrid,
} from "@mui/x-data-grid/locales";
import { useTranslation } from "react-i18next";
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Theme } from "@tauri-apps/api/window";
const languagePackMap: Record<string, any> = {
zh: { ...zhXDataGrid },
fa: { ...faXDataGrid },
ru: { ...ruXDataGrid },
ar: { ...arXDataGrid },
en: { ...enXDataGrid },
};
const getLanguagePackMap = (key: string) =>
languagePackMap[key] || languagePackMap.en;
/**
* custom theme
*/
export const useCustomTheme = () => {
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
const { verge } = useVerge();
const { i18n } = useTranslation();
const { theme_mode, theme_setting } = verge ?? {};
const { theme_mode } = verge ?? {};
const mode = useThemeMode();
const setMode = useSetThemeMode();
useEffect(() => {
if (theme_mode === "light" || theme_mode === "dark") {
setMode(theme_mode);
}
setMode(theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system");
}, [theme_mode, setMode]);
useEffect(() => {
if (theme_mode !== "system") {
return;
}
const root = document.documentElement;
let isMounted = true;
const activeTheme = mode === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
: mode;
const timerId = setTimeout(() => {
if (!isMounted) return;
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0);
root.classList.remove("light", "dark");
root.classList.add(activeTheme);
appWindow.setTheme(activeTheme as Theme).catch(console.error);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
if (isMounted) {
setMode(payload);
}
});
return () => {
isMounted = false;
clearTimeout(timerId);
unlistenPromise
.then((unlistenFn) => {
if (typeof unlistenFn === "function") {
unlistenFn();
}
})
.catch((err) => {
console.error("Failed to unlisten from theme changes:", err);
});
};
}, [theme_mode, appWindow, setMode]);
}, [mode, appWindow]);
useEffect(() => {
if (theme_mode === undefined) {
return;
}
if (theme_mode !== "system") return;
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
setMode(payload);
});
return () => { unlistenPromise.then(f => f()); };
}, [theme_mode, appWindow, setMode]);
if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => {
console.error(
"Failed to set window theme to follow system (setTheme(null)):",
err,
);
});
} else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
console.error(`Failed to set window theme to ${mode}:`, err);
});
}
}, [mode, appWindow, theme_mode]);
const theme = useMemo(() => {
const setting = theme_setting || {};
const dt = mode === "light" ? defaultTheme : defaultDarkTheme;
let muiTheme: MuiTheme;
try {
muiTheme = createTheme(
{
breakpoints: {
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
},
palette: {
mode,
primary: { main: setting.primary_color || dt.primary_color },
secondary: { main: setting.secondary_color || dt.secondary_color },
info: { main: setting.info_color || dt.info_color },
error: { main: setting.error_color || dt.error_color },
warning: { main: setting.warning_color || dt.warning_color },
success: { main: setting.success_color || dt.success_color },
text: {
primary: setting.primary_text || dt.primary_text,
secondary: setting.secondary_text || dt.secondary_text,
},
background: {
paper: dt.background_color,
},
},
shadows: Array(25).fill("none") as Shadows,
typography: {
fontFamily: setting.font_family
? `${setting.font_family}, ${dt.font_family}`
: dt.font_family,
},
},
getLanguagePackMap(i18n.language),
);
} catch (e) {
console.error("Error creating MUI theme, falling back to defaults:", e);
muiTheme = createTheme({
breakpoints: {
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
},
palette: {
mode,
primary: { main: dt.primary_color },
secondary: { main: dt.secondary_color },
info: { main: dt.info_color },
error: { main: dt.error_color },
warning: { main: dt.warning_color },
success: { main: dt.success_color },
text: { primary: dt.primary_text, secondary: dt.secondary_text },
},
typography: { fontFamily: dt.font_family },
});
}
const rootEle = document.documentElement;
if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty(
"--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1),
);
}
// inject css
let styleElement = document.querySelector("style#verge-theme");
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "verge-theme";
document.head.appendChild(styleElement!);
}
if (styleElement) {
styleElement.innerHTML = setting.css_injection || "";
}
const { palette } = muiTheme;
setTimeout(() => {
const dom = document.querySelector("#Gradient2");
if (dom) {
dom.innerHTML = `
<stop offset="0%" stop-color="${palette.primary.main}" />
<stop offset="80%" stop-color="${palette.primary.dark}" />
<stop offset="100%" stop-color="${palette.primary.dark}" />
`;
}
}, 0);
return muiTheme;
}, [mode, theme_setting, i18n.language]);
return { theme };
return {};
};

View File

@@ -1,7 +1,6 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
// Определяем возможные значения темы для TypeScript
type ThemeValue = "light" | "dark" | "system";
interface Props {
@@ -16,7 +15,6 @@ export const ThemeModeSwitch = (props: Props) => {
const modes: ThemeValue[] = ["light", "dark", "system"];
return (
// Создаем ту же самую группу кнопок, что и раньше
<div className="flex items-center rounded-md border bg-muted p-0.5">
{modes.map((mode) => (
<Button
@@ -26,9 +24,6 @@ export const ThemeModeSwitch = (props: Props) => {
size="sm"
className="capitalize px-3 text-xs"
>
{/* Ключевое исправление: мы используем ключи `theme.light`, `theme.dark` и т.д.
Это стандартный подход в i18next для корректной локализации.
*/}
{t(`theme.${mode}`)}
</Button>
))}

View File

@@ -1,7 +1,7 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@variant dark .dark &;
@theme inline {
--radius-sm: calc(var(--radius) - 4px);

View File

@@ -150,7 +150,7 @@ const Layout = () => {
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
useCustomTheme();
const { verge } = useVerge();
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
@@ -160,11 +160,6 @@ const Layout = () => {
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
const initRef = useRef(false);
const [themeReady, setThemeReady] = useState(false);
useEffect(() => {
setThemeReady(true);
}, [theme]);
const handleNotice = useCallback(
(payload: [string, string]) => {
@@ -445,116 +440,18 @@ const Layout = () => {
}
}, [start_page]);
if (!themeReady) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
transition: "background 0.2s",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: mode === "light" ? "#333" : "#fff",
}}
></div>
);
}
if (!routersEles) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: mode === "light" ? "#333" : "#fff",
}}
></div>
);
return <div className="h-screen w-screen bg-background" />;
}
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
<ThemeProvider theme={theme}>
<NoticeManager />
<div
style={{
animation: "fadeIn 0.5s",
WebkitAnimation: "fadeIn 0.5s",
}}
/>
<style>
{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}
</style>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */}
{/* 1. Убрали класс "layout" с компонента Paper */}
<Paper
square
elevation={0}
className={OS} // Был: className={`${OS} layout`}
style={{
borderTopLeftRadius: "0px",
borderTopRightRadius: "0px",
// Добавляем стили, чтобы контейнер занимал все пространство
width: "100vw",
height: "100vh",
display: "flex", // Используем flex, чтобы контент растянулся
flexDirection: "column",
}}
onContextMenu={(e) => {
if (
OS === "windows" &&
!["input", "textarea"].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault();
}
}}
sx={[
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux"
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
}
: {},
]}
>
{/* 2. Левая колонка <div className="layout__left">...</div> ПОЛНОСТЬЮ УДАЛЕНА */}
{/* 3. Оставляем только "правую" часть, которая теперь станет основной */}
{/* и заставляем ее занять все доступное место */}
<div
className="main-content-area"
style={{ flex: 1, display: "flex", flexDirection: "column" }}
>
{/* 4. Бар-разделитель <div className="the-bar"></div> тоже удален, он больше не нужен */}
<div
className="the-content"
style={{ flex: 1, position: "relative" }}
>
{React.cloneElement(routersEles, { key: location.pathname })}
</div>
</div>
</Paper>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
</ThemeProvider>
<NoticeManager />
<div className="h-screen w-screen bg-background text-foreground overflow-hidden">
<div className="h-full w-full relative">
{React.cloneElement(routersEles, { key: location.pathname })}
</div>
</div>
</SWRConfig>
);
};

View File

@@ -2,8 +2,8 @@ import { createContextState } from "foxact/create-context-state";
import { useLocalStorage } from "foxact/use-local-storage";
const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
"light" | "dark"
>("light");
"light" | "dark" | "system"
>("system");
export const useEnableLog = () => useLocalStorage("enable-log", false);