mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 13:59:03 +08:00
244 lines
7.0 KiB
TypeScript
244 lines
7.0 KiB
TypeScript
import { Service } from '../../shared/kernel'
|
|
import { MainContext } from '../context'
|
|
import { BrowserWindow } from 'electron'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { FSWatcher, watch } from 'chokidar'
|
|
|
|
export interface themeConfig {
|
|
name: string
|
|
id: string
|
|
mode: 'light' | 'dark'
|
|
mica?: {
|
|
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
theme: 'auto' | 'dark' | 'light'
|
|
radius: 'small' | 'medium' | 'large'
|
|
}
|
|
config: {
|
|
tdesign: Record<string, string>
|
|
custom: Record<string, string>
|
|
}
|
|
}
|
|
|
|
declare module '../../shared/kernel' {
|
|
interface Context {
|
|
themes: ThemeService
|
|
}
|
|
}
|
|
|
|
export class ThemeService extends Service {
|
|
private themeDir: string
|
|
private watcher: FSWatcher | null = null
|
|
private currentThemeId: string = 'light-default'
|
|
|
|
constructor(ctx: MainContext, themeDir: string) {
|
|
super(ctx, 'themes')
|
|
this.themeDir = themeDir
|
|
this.setupWatcher()
|
|
this.registerIpc()
|
|
|
|
ctx.effect(() => {
|
|
if (this.watcher) this.watcher.close()
|
|
})
|
|
}
|
|
|
|
private get mainCtx() {
|
|
return this.ctx as MainContext
|
|
}
|
|
|
|
public async init() {
|
|
await this.loadSavedTheme()
|
|
}
|
|
|
|
private async loadSavedTheme() {
|
|
try {
|
|
const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any)
|
|
if (savedThemeId && typeof savedThemeId === 'string') {
|
|
const themes = this.getThemeList()
|
|
const exists = themes.some((t) => t.id === savedThemeId)
|
|
if (exists) {
|
|
this.currentThemeId = savedThemeId
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.mainCtx.logger.warn('Failed to load saved theme', { meta: e })
|
|
}
|
|
}
|
|
|
|
private async saveCurrentTheme() {
|
|
try {
|
|
await this.mainCtx.settings.setValue('current_theme_id' as any, this.currentThemeId)
|
|
} catch (e) {
|
|
this.mainCtx.logger.warn('Failed to save theme', { meta: e })
|
|
}
|
|
}
|
|
|
|
private setupWatcher() {
|
|
if (this.watcher) this.watcher.close()
|
|
|
|
this.watcher = watch(this.themeDir, {
|
|
ignored: /(^|[/\\])\../,
|
|
persistent: true
|
|
})
|
|
|
|
this.watcher.on('change', (filePath) => {
|
|
if (filePath.endsWith('.json')) {
|
|
this.mainCtx.logger.info('Theme file changed', { filePath })
|
|
this.notifyThemeUpdate()
|
|
}
|
|
})
|
|
}
|
|
|
|
private registerIpc() {
|
|
this.mainCtx.handle('theme:list', async () => {
|
|
return { success: true, data: this.getThemeList() }
|
|
})
|
|
|
|
this.mainCtx.handle('theme:current', async () => {
|
|
const theme = this.getThemeById(this.currentThemeId)
|
|
return { success: true, data: theme }
|
|
})
|
|
|
|
this.mainCtx.handle('theme:set', async (event, themeId: string) => {
|
|
const senderId = event?.sender?.id
|
|
if (typeof senderId === 'number') {
|
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
|
return { success: false, message: 'Permission denied' }
|
|
}
|
|
this.currentThemeId = themeId
|
|
await this.saveCurrentTheme()
|
|
this.applyMicaEffect(themeId)
|
|
this.notifyThemeUpdate()
|
|
return { success: true }
|
|
})
|
|
|
|
this.mainCtx.handle('theme:save', async (event, theme: themeConfig) => {
|
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
|
return { success: false, message: 'Permission denied' }
|
|
|
|
try {
|
|
const filePath = path.join(this.themeDir, `${theme.id}.json`)
|
|
fs.writeFileSync(filePath, JSON.stringify(theme, null, 2), 'utf-8')
|
|
this.notifyThemeUpdate()
|
|
return { success: true }
|
|
} catch (e) {
|
|
return { success: false, message: String(e) }
|
|
}
|
|
})
|
|
|
|
this.mainCtx.handle('theme:delete', async (event, themeId: string) => {
|
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
|
return { success: false, message: 'Permission denied' }
|
|
|
|
if (themeId.startsWith('default-')) {
|
|
return { success: false, message: 'Cannot delete default themes' }
|
|
}
|
|
|
|
try {
|
|
const filePath = path.join(this.themeDir, `${themeId}.json`)
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath)
|
|
}
|
|
if (this.currentThemeId === themeId) {
|
|
this.currentThemeId = 'light' // Fallback
|
|
}
|
|
this.notifyThemeUpdate()
|
|
return { success: true }
|
|
} catch (e) {
|
|
return { success: false, message: String(e) }
|
|
}
|
|
})
|
|
|
|
this.mainCtx.handle(
|
|
'theme:set-custom',
|
|
async (
|
|
event,
|
|
config: {
|
|
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
theme: 'auto' | 'dark' | 'light'
|
|
radius: 'small' | 'medium' | 'large'
|
|
}
|
|
) => {
|
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
|
return { success: false, message: 'Permission denied' }
|
|
|
|
this.currentThemeId = 'custom'
|
|
await this.saveCurrentTheme()
|
|
this.applyMicaConfig(config)
|
|
this.notifyThemeUpdate()
|
|
return { success: true }
|
|
}
|
|
)
|
|
}
|
|
|
|
private applyMicaEffect(themeId: string) {
|
|
const theme = this.getThemeById(themeId)
|
|
if (!theme?.mica) return
|
|
|
|
this.applyMicaConfig(theme.mica)
|
|
}
|
|
|
|
private applyMicaConfig(config: {
|
|
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
theme: 'auto' | 'dark' | 'light'
|
|
radius: 'small' | 'medium' | 'large'
|
|
}) {
|
|
const radiusMap: Record<string, 'rounded' | 'small' | 'square'> = {
|
|
small: 'small',
|
|
medium: 'rounded',
|
|
large: 'rounded'
|
|
}
|
|
|
|
const windows = BrowserWindow.getAllWindows()
|
|
for (const win of windows) {
|
|
if (win.isDestroyed()) continue
|
|
|
|
this.mainCtx.windows.setMicaEffect(win, config.effect)
|
|
this.mainCtx.windows.setMicaTheme(win, config.theme)
|
|
this.mainCtx.windows.setMicaCorner(win, radiusMap[config.radius] || 'rounded')
|
|
}
|
|
|
|
this.mainCtx.settings.setValue('window_effect', config.effect)
|
|
this.mainCtx.settings.setValue('window_theme', config.theme)
|
|
this.mainCtx.settings.setValue('window_radius', radiusMap[config.radius] || 'rounded')
|
|
}
|
|
|
|
private getThemeList(): themeConfig[] {
|
|
try {
|
|
if (!fs.existsSync(this.themeDir)) return []
|
|
const files = fs.readdirSync(this.themeDir)
|
|
return files
|
|
.filter((f) => f.endsWith('.json'))
|
|
.map((f) => {
|
|
try {
|
|
const content = fs.readFileSync(path.join(this.themeDir, f), 'utf-8')
|
|
return JSON.parse(content) as themeConfig
|
|
} catch {
|
|
return null
|
|
}
|
|
})
|
|
.filter((t): t is themeConfig => t !== null)
|
|
} catch (e) {
|
|
this.mainCtx.logger.error('Failed to read themes', {
|
|
meta: e instanceof Error ? { message: e.message, stack: e.stack } : { e }
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
private getThemeById(id: string): themeConfig | null {
|
|
const list = this.getThemeList()
|
|
return list.find((t) => t.id === id) || list[0] || null
|
|
}
|
|
|
|
private notifyThemeUpdate() {
|
|
const theme = this.getThemeById(this.currentThemeId)
|
|
if (!theme) return
|
|
|
|
const windows = BrowserWindow.getAllWindows()
|
|
for (const win of windows) {
|
|
win.webContents.send('theme:updated', theme)
|
|
}
|
|
}
|
|
}
|