fixed dark mode
This commit is contained in:
10
src/App.tsx
10
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 (
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
</AppDataProvider>
|
||||
<ThemeProvider>
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
</AppDataProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
105
src/components/layout/theme-provider.tsx
Normal file
105
src/components/layout/theme-provider.tsx
Normal 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}</>;
|
||||
}
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user