mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-17 05:30:59 +08:00
feat: 增强主题管理,支持动态主题切换和持久化设置
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { applyDocumentThemeChrome } from '@/utils/themePalette'
|
||||
|
||||
// 主题管理器 - 动态加载主题CSS
|
||||
export interface ThemeConfig {
|
||||
name: string
|
||||
@@ -116,18 +118,13 @@ class ThemeManager {
|
||||
* 应用主题到DOM
|
||||
*/
|
||||
private applyTheme(themeName: string): void {
|
||||
// 移除之前的主题属性
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
|
||||
// 设置新主题(除了default主题)
|
||||
if (themeName !== 'default') {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
}
|
||||
// auto 是用户偏好,DOM 上必须落到实际主题,避免恢复前台时短暂匹配不到深色样式。
|
||||
const { resolvedTheme } = applyDocumentThemeChrome(themeName)
|
||||
|
||||
this.currentTheme = themeName
|
||||
|
||||
// 触发主题变更事件
|
||||
this.dispatchThemeChangeEvent(themeName)
|
||||
this.dispatchThemeChangeEvent(resolvedTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
125
src/utils/themePalette.ts
Normal file
125
src/utils/themePalette.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
|
||||
export type ThemePreference = 'auto' | 'default' | 'light' | 'dark' | 'purple' | 'transparent'
|
||||
export type ResolvedThemeName = 'light' | 'dark' | 'purple' | 'transparent'
|
||||
export type ThemeColorScheme = 'light' | 'dark'
|
||||
|
||||
interface ThemeRootPalette {
|
||||
background: string
|
||||
primary: string
|
||||
}
|
||||
|
||||
interface ApplyDocumentThemeChromeOptions {
|
||||
background?: string
|
||||
persistLoaderColors?: boolean
|
||||
primary?: string
|
||||
resolvedTheme?: string
|
||||
}
|
||||
|
||||
export const themeRootPalettes: Record<ResolvedThemeName, ThemeRootPalette> = {
|
||||
light: {
|
||||
background: '#F4F5FA',
|
||||
primary: '#9155FD',
|
||||
},
|
||||
dark: {
|
||||
background: '#0E1116',
|
||||
primary: '#6E66ED',
|
||||
},
|
||||
purple: {
|
||||
background: '#28243D',
|
||||
primary: '#9155FD',
|
||||
},
|
||||
transparent: {
|
||||
background: '#1C1C1C',
|
||||
primary: '#A370F7',
|
||||
},
|
||||
}
|
||||
|
||||
const validResolvedThemes = new Set<string>(Object.keys(themeRootPalettes))
|
||||
|
||||
function normalizeResolvedThemeName(themeName: string | null | undefined): ResolvedThemeName {
|
||||
return validResolvedThemes.has(themeName || '') ? (themeName as ResolvedThemeName) : 'light'
|
||||
}
|
||||
|
||||
export function resolveThemeName(themePreference: string | null | undefined): ResolvedThemeName {
|
||||
if (themePreference === 'auto') {
|
||||
return checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
if (themePreference === 'default') {
|
||||
return 'light'
|
||||
}
|
||||
|
||||
return normalizeResolvedThemeName(themePreference)
|
||||
}
|
||||
|
||||
export function getThemeColorScheme(themeName: string | null | undefined): ThemeColorScheme {
|
||||
return ['dark', 'purple', 'transparent'].includes(themeName || '') ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function setMetaContent(selector: string, content: string) {
|
||||
document.querySelectorAll<HTMLMetaElement>(selector).forEach(meta => {
|
||||
meta.content = content
|
||||
})
|
||||
}
|
||||
|
||||
function ensureThemeColorMeta(themeColor: string) {
|
||||
const metas = document.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
|
||||
if (metas.length) {
|
||||
metas.forEach(meta => {
|
||||
meta.content = themeColor
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const meta = document.createElement('meta')
|
||||
meta.name = 'theme-color'
|
||||
meta.content = themeColor
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步浏览器首帧会使用的根节点底色和系统控件配色。
|
||||
* iOS PWA 从后台恢复时可能先绘制 WebView 外壳,再等 Vue 响应式主题更新。
|
||||
*/
|
||||
export function applyDocumentThemeChrome(
|
||||
themePreference: string | null | undefined,
|
||||
options: ApplyDocumentThemeChromeOptions = {},
|
||||
) {
|
||||
const resolvedTheme = normalizeResolvedThemeName(options.resolvedTheme || resolveThemeName(themePreference))
|
||||
const colorScheme = getThemeColorScheme(resolvedTheme)
|
||||
const palette = themeRootPalettes[resolvedTheme]
|
||||
const background = options.background || palette.background
|
||||
const primary = options.primary || palette.primary
|
||||
|
||||
document.documentElement.setAttribute('data-theme', resolvedTheme)
|
||||
document.documentElement.setAttribute('data-theme-preference', themePreference || resolvedTheme)
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', background)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primary)
|
||||
document.documentElement.style.backgroundColor = background
|
||||
document.documentElement.style.colorScheme = colorScheme
|
||||
|
||||
if (document.body) {
|
||||
document.body.setAttribute('data-theme', resolvedTheme)
|
||||
document.body.setAttribute('data-theme-preference', themePreference || resolvedTheme)
|
||||
document.body.style.backgroundColor = background
|
||||
document.body.style.colorScheme = colorScheme
|
||||
}
|
||||
|
||||
setMetaContent('meta[name="color-scheme"]', colorScheme === 'dark' ? 'dark light' : 'light dark')
|
||||
ensureThemeColorMeta(background)
|
||||
|
||||
if (options.persistLoaderColors) {
|
||||
localStorage.setItem('materio-initial-loader-bg', background)
|
||||
localStorage.setItem('materio-initial-loader-color', primary)
|
||||
}
|
||||
|
||||
return {
|
||||
background,
|
||||
colorScheme,
|
||||
primary,
|
||||
resolvedTheme,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user