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 }
}