import { alpha, createTheme, Theme as MuiTheme, Shadows } from '@mui/material' import { getCurrentWebviewWindow, WebviewWindow, } from '@tauri-apps/api/webviewWindow' import { Theme as TauriOsTheme } from '@tauri-apps/api/window' import { useEffect, useMemo } from 'react' import { useVerge } from '@/hooks/use-verge' import { defaultDarkTheme, defaultTheme } from '@/pages/_theme' import { useSetThemeMode, useThemeMode } from '@/services/states' const CSS_INJECTION_SCOPE_ROOT = '[data-css-injection-root]' const CSS_INJECTION_SCOPE_LIMIT = ':is(.monaco-editor .view-lines, .monaco-editor .view-line, .monaco-editor .margin, .monaco-editor .margin-view-overlays, .monaco-editor .view-overlays, .monaco-editor [class^="mtk"], .monaco-editor [class*=" mtk"])' const TOP_LEVEL_AT_RULES = [ '@charset', '@import', '@namespace', '@font-face', '@keyframes', '@counter-style', '@page', '@property', '@font-feature-values', '@color-profile', ] let cssScopeSupport: boolean | null = null const canUseCssScope = () => { if (cssScopeSupport !== null) { return cssScopeSupport } try { const testStyle = document.createElement('style') testStyle.textContent = '@scope (:root) { }' document.head.appendChild(testStyle) cssScopeSupport = !!testStyle.sheet?.cssRules?.length document.head.removeChild(testStyle) } catch { cssScopeSupport = false } return cssScopeSupport } const wrapCssInjectionWithScope = (css?: string) => { if (!css?.trim()) { return '' } const lowerCss = css.toLowerCase() const hasTopLevelOnlyRule = TOP_LEVEL_AT_RULES.some((rule) => lowerCss.includes(rule), ) if (hasTopLevelOnlyRule) { return null } const scopeRoot = CSS_INJECTION_SCOPE_ROOT const scopeLimit = CSS_INJECTION_SCOPE_LIMIT const scopedBlock = `@scope (${scopeRoot}) to (${scopeLimit}) { ${css} }` return scopedBlock } /** * custom theme */ export const useCustomTheme = () => { const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []) const { verge } = useVerge() const { theme_mode, theme_setting } = verge ?? {} const mode = useThemeMode() const setMode = useSetThemeMode() const userBackgroundImage = theme_setting?.background_image || '' const hasUserBackground = !!userBackgroundImage useEffect(() => { if (theme_mode === 'light' || theme_mode === 'dark') { setMode(theme_mode) } }, [theme_mode, setMode]) useEffect(() => { if (theme_mode !== 'system') { return } let isMounted = true 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) 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]) useEffect(() => { if (theme_mode === undefined) { return } 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, default: dt.background_color, }, }, shadows: Array(25).fill('none') as Shadows, typography: { fontFamily: setting.font_family ? `${setting.font_family}, ${dt.font_family}` : dt.font_family, }, }) } 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 }, background: { paper: dt.background_color, default: dt.background_color, }, }, typography: { fontFamily: dt.font_family }, }) } const rootEle = document.documentElement if (rootEle) { const backgroundColor = mode === 'light' ? '#ECECEC' : dt.background_color const selectColor = mode === 'light' ? '#f5f5f5' : '#3E3E3E' const scrollColor = mode === 'light' ? '#90939980' : '#555555' 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), ) rootEle.style.setProperty( '--window-border-color', mode === 'light' ? '#cccccc' : '#1E1E1E', ) rootEle.style.setProperty( '--scrollbar-bg', mode === 'light' ? '#f1f1f1' : '#2E303D', ) rootEle.style.setProperty( '--scrollbar-thumb', mode === 'light' ? '#c1c1c1' : '#555555', ) rootEle.style.setProperty( '--user-background-image', hasUserBackground ? `url('${userBackgroundImage}')` : 'none', ) rootEle.style.setProperty( '--background-blend-mode', setting.background_blend_mode || 'normal', ) rootEle.style.setProperty( '--background-opacity', setting.background_opacity !== undefined ? String(setting.background_opacity) : '1', ) rootEle.setAttribute('data-css-injection-root', 'true') } let styleElement = document.querySelector('style#verge-theme') if (!styleElement) { styleElement = document.createElement('style') styleElement.id = 'verge-theme' document.head.appendChild(styleElement!) } if (styleElement) { let scopedCss: string | null = null if (canUseCssScope() && setting.css_injection) { scopedCss = wrapCssInjectionWithScope(setting.css_injection) } const effectiveInjectedCss = scopedCss ?? setting.css_injection ?? '' const globalStyles = ` /* 修复滚动条样式 */ ::-webkit-scrollbar { width: 8px; height: 8px; background-color: var(--scrollbar-bg); } ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background-color: ${mode === 'light' ? '#a1a1a1' : '#666666'}; } /* 背景图处理 */ body { background-color: var(--background-color); ${ hasUserBackground ? ` background-image: var(--user-background-image); background-size: cover; background-position: center; background-attachment: fixed; background-blend-mode: var(--background-blend-mode); opacity: var(--background-opacity); ` : '' } } /* 修复可能的白色边框 */ .MuiPaper-root { border-color: var(--window-border-color) !important; } /* 确保模态框和对话框也使用暗色主题 */ .MuiDialog-paper { background-color: ${mode === 'light' ? '#ffffff' : '#2E303D'} !important; } /* 移除可能的白色点或线条 */ * { outline: none !important; box-shadow: none !important; } ` styleElement.innerHTML = effectiveInjectedCss + globalStyles } const { palette } = muiTheme setTimeout(() => { const dom = document.querySelector('#Gradient2') if (dom) { dom.innerHTML = ` ` } }, 0) return muiTheme }, [mode, theme_setting, userBackgroundImage, hasUserBackground]) return { theme } }