feat(ui): 主题切换 + 语言持久化 + About 对话框
- 新增 light/dark 主题调色板与 Windows 注册表系统主题探测 - 偏好设置对话框:主题三选一 + 语言下拉(en/zh-CN/de),即时生效 - 持久化 Language/Theme/AutoLockMinutes/MinimizeToTray 至 YAML 顶层 - 启动时按 config 中保存的语言初始化 i18n - About 对话框展示 version/runtime/项目地址/许可证 - 全部硬编码颜色迁移到 themePalette,为深色模式做准备
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Package ui internal helpers — theme palette + system-theme detection.
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
// themeMode is one of the user-selectable theme settings.
|
||||
type themeMode string
|
||||
|
||||
const (
|
||||
themeSystem themeMode = "system"
|
||||
themeLight themeMode = "light"
|
||||
themeDark themeMode = "dark"
|
||||
)
|
||||
|
||||
// themePalette holds the small set of colors the app itself draws
|
||||
// (toast, dividers, ring background, etc). The material.Theme handles
|
||||
// button + body text colors via its own palette.
|
||||
type themePalette struct {
|
||||
Background color.NRGBA
|
||||
OnBackground color.NRGBA
|
||||
Divider color.NRGBA
|
||||
RingBg color.NRGBA
|
||||
RingFg color.NRGBA
|
||||
RingFgWarn color.NRGBA
|
||||
ToastBg color.NRGBA
|
||||
ToastFg color.NRGBA
|
||||
ErrorFg color.NRGBA
|
||||
DialogBg color.NRGBA
|
||||
DialogBorder color.NRGBA
|
||||
ScrimBg color.NRGBA // backdrop behind modal dialogs
|
||||
MutedFg color.NRGBA // for placeholder / hint text
|
||||
}
|
||||
|
||||
func lightPalette() themePalette {
|
||||
return themePalette{
|
||||
Background: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
|
||||
OnBackground: color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff},
|
||||
Divider: color.NRGBA{R: 0xe0, G: 0xe0, B: 0xe0, A: 0xff},
|
||||
RingBg: color.NRGBA{R: 0xd8, G: 0xd8, B: 0xd8, A: 0xff},
|
||||
RingFg: color.NRGBA{R: 0x10, G: 0x70, B: 0xff, A: 0xff},
|
||||
RingFgWarn: color.NRGBA{R: 0xd0, G: 0x30, B: 0x30, A: 0xff},
|
||||
ToastBg: color.NRGBA{R: 0x20, G: 0x20, B: 0x20, A: 0xe0},
|
||||
ToastFg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
|
||||
ErrorFg: color.NRGBA{R: 0xc0, G: 0x00, B: 0x00, A: 0xff},
|
||||
DialogBg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
|
||||
DialogBorder: color.NRGBA{R: 0x55, G: 0x55, B: 0x55, A: 0xff},
|
||||
ScrimBg: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x60},
|
||||
MutedFg: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff},
|
||||
}
|
||||
}
|
||||
|
||||
func darkPalette() themePalette {
|
||||
return themePalette{
|
||||
Background: color.NRGBA{R: 0x1e, G: 0x1e, B: 0x1e, A: 0xff},
|
||||
OnBackground: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff},
|
||||
Divider: color.NRGBA{R: 0x38, G: 0x38, B: 0x38, A: 0xff},
|
||||
RingBg: color.NRGBA{R: 0x3a, G: 0x3a, B: 0x3a, A: 0xff},
|
||||
RingFg: color.NRGBA{R: 0x4d, G: 0xa3, B: 0xff, A: 0xff},
|
||||
RingFgWarn: color.NRGBA{R: 0xff, G: 0x6b, B: 0x6b, A: 0xff},
|
||||
ToastBg: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xee},
|
||||
ToastFg: color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff},
|
||||
ErrorFg: color.NRGBA{R: 0xff, G: 0x6b, B: 0x6b, A: 0xff},
|
||||
DialogBg: color.NRGBA{R: 0x2a, G: 0x2a, B: 0x2a, A: 0xff},
|
||||
DialogBorder: color.NRGBA{R: 0x60, G: 0x60, B: 0x60, A: 0xff},
|
||||
ScrimBg: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x90},
|
||||
MutedFg: color.NRGBA{R: 0x90, G: 0x90, B: 0x90, A: 0xff},
|
||||
}
|
||||
}
|
||||
|
||||
// buildTheme returns a fresh material.Theme + companion palette for the
|
||||
// requested mode. "system" resolves via systemPrefersDark (Windows
|
||||
// registry on win, false elsewhere).
|
||||
func buildTheme(mode themeMode) (*material.Theme, themePalette) {
|
||||
resolved := mode
|
||||
if resolved == "" || resolved == themeSystem {
|
||||
if systemPrefersDark() {
|
||||
resolved = themeDark
|
||||
} else {
|
||||
resolved = themeLight
|
||||
}
|
||||
}
|
||||
var pal themePalette
|
||||
if resolved == themeDark {
|
||||
pal = darkPalette()
|
||||
} else {
|
||||
pal = lightPalette()
|
||||
}
|
||||
|
||||
th := material.NewTheme()
|
||||
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
th.Palette.Bg = pal.Background
|
||||
th.Palette.Fg = pal.OnBackground
|
||||
if resolved == themeDark {
|
||||
th.Palette.ContrastBg = color.NRGBA{R: 0x4d, G: 0xa3, B: 0xff, A: 0xff}
|
||||
th.Palette.ContrastFg = color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff}
|
||||
}
|
||||
return th, pal
|
||||
}
|
||||
|
||||
// normalizeThemeMode coerces an arbitrary config string into one of the
|
||||
// known modes. Unknown values fall back to "system".
|
||||
func normalizeThemeMode(s string) themeMode {
|
||||
switch themeMode(s) {
|
||||
case themeLight, themeDark, themeSystem:
|
||||
return themeMode(s)
|
||||
}
|
||||
return themeSystem
|
||||
}
|
||||
|
||||
// activePalette holds the palette of the currently-rendered theme. Set
|
||||
// once per frame at the top of drawFrame so shared helpers (toast,
|
||||
// dividers, dialog backdrops) can colour themselves without dragging a
|
||||
// palette argument through every signature. Gio invokes drawFrame on
|
||||
// the single UI goroutine so plain assignment is safe.
|
||||
var activePalette = lightPalette()
|
||||
Reference in New Issue
Block a user