mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from "react"
|
|
import { themeConfig } from "../preload/types"
|
|
import { generateColorMap } from "../utils/color"
|
|
|
|
interface themeContextType {
|
|
currentTheme: themeConfig | null
|
|
setTheme: (id: string) => Promise<void>
|
|
themes: themeConfig[]
|
|
applyTheme: (theme: themeConfig) => void
|
|
}
|
|
|
|
const ThemeContext = createContext<themeContextType | undefined>(undefined)
|
|
|
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [currentTheme, setCurrentTheme] = useState<themeConfig | null>(null)
|
|
const [themes, setThemes] = useState<themeConfig[]>([])
|
|
const appliedStyleKeysRef = useRef<string[]>([])
|
|
const currentThemeRef = useRef<themeConfig | null>(null)
|
|
|
|
const applyThemeConfig = useCallback((theme: themeConfig) => {
|
|
if (!theme) return
|
|
const tdesign = theme.config?.tdesign || {}
|
|
const custom = theme.config?.custom || {}
|
|
const root = document.documentElement
|
|
const prevKeys = appliedStyleKeysRef.current
|
|
for (const k of prevKeys) root.style.removeProperty(k)
|
|
const nextKeys: string[] = []
|
|
|
|
root.setAttribute("theme-mode", theme.mode || "light")
|
|
|
|
const brandColor = tdesign.brandColor || "#1677FF"
|
|
const hex = brandColor.replace("#", "")
|
|
const r = parseInt(hex.substring(0, 2), 16)
|
|
const g = parseInt(hex.substring(2, 4), 16)
|
|
const b = parseInt(hex.substring(4, 6), 16)
|
|
|
|
root.style.setProperty("--ss-primary-color", brandColor)
|
|
root.style.setProperty("--ss-primary-rgb", `${r}, ${g}, ${b}`)
|
|
nextKeys.push("--ss-primary-color")
|
|
nextKeys.push("--ss-primary-rgb")
|
|
|
|
if (brandColor) {
|
|
const colorMap = generateColorMap(brandColor, theme.mode || "light")
|
|
Object.entries(colorMap).forEach(([key, value]) => {
|
|
root.style.setProperty(key, value)
|
|
nextKeys.push(key)
|
|
})
|
|
|
|
root.style.setProperty("--ant-color-primary", brandColor)
|
|
nextKeys.push("--ant-color-primary")
|
|
|
|
root.style.setProperty("--ant-color-primary-hover", `rgba(${r}, ${g}, ${b}, 0.85)`)
|
|
root.style.setProperty("--ant-color-primary-active", `rgba(${r}, ${g}, ${b}, 0.7)`)
|
|
root.style.setProperty("--ant-color-primary-bg", `rgba(${r}, ${g}, ${b}, 0.1)`)
|
|
root.style.setProperty("--ant-color-primary-bg-hover", `rgba(${r}, ${g}, ${b}, 0.2)`)
|
|
root.style.setProperty("--ant-color-primary-border", `rgba(${r}, ${g}, ${b}, 0.3)`)
|
|
|
|
nextKeys.push("--ant-color-primary-hover")
|
|
nextKeys.push("--ant-color-primary-active")
|
|
nextKeys.push("--ant-color-primary-bg")
|
|
nextKeys.push("--ant-color-primary-bg-hover")
|
|
nextKeys.push("--ant-color-primary-border")
|
|
}
|
|
|
|
if (tdesign.warningColor) {
|
|
root.style.setProperty("--ant-color-warning", tdesign.warningColor)
|
|
nextKeys.push("--ant-color-warning")
|
|
}
|
|
if (tdesign.errorColor) {
|
|
root.style.setProperty("--ant-color-error", tdesign.errorColor)
|
|
nextKeys.push("--ant-color-error")
|
|
}
|
|
if (tdesign.successColor) {
|
|
root.style.setProperty("--ant-color-success", tdesign.successColor)
|
|
nextKeys.push("--ant-color-success")
|
|
}
|
|
|
|
Object.entries(custom).forEach(([key, value]) => {
|
|
root.style.setProperty(key, String(value))
|
|
nextKeys.push(key)
|
|
})
|
|
|
|
const bgLight = custom["--ss-bg-color-light"]
|
|
const bgDark = custom["--ss-bg-color-dark"]
|
|
if (bgLight || bgDark) {
|
|
const resolved = theme.mode === "dark" ? bgDark || bgLight : bgLight || bgDark
|
|
if (resolved) {
|
|
root.style.setProperty("--ss-bg-color", resolved)
|
|
nextKeys.push("--ss-bg-color")
|
|
}
|
|
}
|
|
|
|
if (!custom["--ss-sidebar-active-bg"]) {
|
|
const alpha = theme.mode === "dark" ? 0.22 : 0.12
|
|
root.style.setProperty("--ss-sidebar-active-bg", `rgba(${r}, ${g}, ${b}, ${alpha})`)
|
|
nextKeys.push("--ss-sidebar-active-bg")
|
|
}
|
|
if (!custom["--ss-sidebar-active-text"]) {
|
|
root.style.setProperty("--ss-sidebar-active-text", brandColor)
|
|
nextKeys.push("--ss-sidebar-active-text")
|
|
}
|
|
|
|
appliedStyleKeysRef.current = nextKeys
|
|
}, [])
|
|
|
|
const loadThemes = useCallback(async () => {
|
|
if (!(window as any).api) return
|
|
const res = await (window as any).api.getThemes()
|
|
if (res.success && res.data) {
|
|
setThemes(res.data)
|
|
}
|
|
}, [])
|
|
|
|
const loadCurrentTheme = useCallback(async () => {
|
|
if (!(window as any).api) return
|
|
const res = await (window as any).api.getCurrentTheme()
|
|
if (res.success && res.data) {
|
|
setCurrentTheme(res.data)
|
|
currentThemeRef.current = res.data
|
|
applyThemeConfig(res.data)
|
|
}
|
|
}, [applyThemeConfig])
|
|
|
|
useEffect(() => {
|
|
const api = (window as any).api
|
|
if (!api) return
|
|
|
|
;(async () => {
|
|
await loadThemes()
|
|
await loadCurrentTheme()
|
|
})()
|
|
|
|
let disposed = false
|
|
let unlisten: (() => void) | null = null
|
|
|
|
api
|
|
.onThemeChanged((theme: themeConfig) => {
|
|
setCurrentTheme(theme)
|
|
currentThemeRef.current = theme
|
|
applyThemeConfig(theme)
|
|
loadThemes()
|
|
})
|
|
.then((fn: () => void) => {
|
|
if (disposed) {
|
|
fn()
|
|
return
|
|
}
|
|
unlisten = fn
|
|
})
|
|
.catch(() => void 0)
|
|
|
|
return () => {
|
|
disposed = true
|
|
if (unlisten) unlisten()
|
|
}
|
|
}, [applyThemeConfig, loadCurrentTheme, loadThemes])
|
|
|
|
const setTheme = async (id: string) => {
|
|
const res = await (window as any).api.setTheme(id)
|
|
if (res.success) {
|
|
await loadCurrentTheme()
|
|
}
|
|
}
|
|
|
|
const applyTheme = useCallback(
|
|
(theme: themeConfig) => {
|
|
applyThemeConfig(theme)
|
|
},
|
|
[applyThemeConfig]
|
|
)
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ currentTheme, setTheme, themes, applyTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
)
|
|
}
|
|
|
|
export const useTheme = () => {
|
|
const context = useContext(ThemeContext)
|
|
if (!context) throw new Error("useTheme must be used within ThemeProvider")
|
|
return context
|
|
}
|