From ec99e24ca103f971e9ec339ac3de67c661543bc3 Mon Sep 17 00:00:00 2001 From: coolcoala Date: Wed, 9 Jul 2025 04:43:16 +0300 Subject: [PATCH] fixed dark mode --- src/App.tsx | 10 +- src/components/layout/theme-provider.tsx | 105 +++++++++ src/components/layout/use-custom-theme.ts | 214 ++---------------- .../setting/mods/theme-mode-switch.tsx | 5 - src/index.css | 2 +- src/pages/_layout.tsx | 119 +--------- src/services/states.ts | 4 +- 7 files changed, 143 insertions(+), 316 deletions(-) create mode 100644 src/components/layout/theme-provider.tsx diff --git a/src/App.tsx b/src/App.tsx index 2ef77bb4..5e77f70d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( - - - + + + + + ); } - export default App; diff --git a/src/components/layout/theme-provider.tsx b/src/components/layout/theme-provider.tsx new file mode 100644 index 00000000..77987421 --- /dev/null +++ b/src/components/layout/theme-provider.tsx @@ -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; // тег + 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}; +} diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts index 0a17967b..27a029c1 100644 --- a/src/components/layout/use-custom-theme.ts +++ b/src/components/layout/use-custom-theme.ts @@ -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 = { - 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 = ` - - - - `; - } - }, 0); - - return muiTheme; - }, [mode, theme_setting, i18n.language]); - - return { theme }; + return {}; }; diff --git a/src/components/setting/mods/theme-mode-switch.tsx b/src/components/setting/mods/theme-mode-switch.tsx index 0be07458..8d63dc12 100644 --- a/src/components/setting/mods/theme-mode-switch.tsx +++ b/src/components/setting/mods/theme-mode-switch.tsx @@ -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 ( - // Создаем ту же самую группу кнопок, что и раньше
{modes.map((mode) => ( ))} diff --git a/src/index.css b/src/index.css index a63722b9..4a1fa7c0 100644 --- a/src/index.css +++ b/src/index.css @@ -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); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index ef635375..84c065fa 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -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 ( -
- ); - } - if (!routersEles) { - return ( -
- ); + return
; } return ( - - -
- - - {/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */} - - {/* 1. Убрали класс "layout" с компонента Paper */} - { - 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. Левая колонка
...
ПОЛНОСТЬЮ УДАЛЕНА */} - - {/* 3. Оставляем только "правую" часть, которая теперь станет основной */} - {/* и заставляем ее занять все доступное место */} -
- {/* 4. Бар-разделитель
тоже удален, он больше не нужен */} - -
- {React.cloneElement(routersEles, { key: location.pathname })} -
-
-
- - {/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */} - + +
+
+ {React.cloneElement(routersEles, { key: location.pathname })} +
+
); }; diff --git a/src/services/states.ts b/src/services/states.ts index 7bd63ef9..3c7b2ed3 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -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);