Files
u-desktop/settings.go

599 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
_ "embed"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"unsafe"
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
//go:embed web/settings.html
var settingsHTML string
var (
settingsMu sync.Mutex
settingsWv webview2.WebView // 持久实例nil 直到首次打开
settingsHwnd uintptr
settingsOldWndProc uintptr // 子类化前的原始 wndproc
settingsWndProcCb uintptr // NewCallback 指针,防止 GC
settingsCreated bool // 首次创建完成后 true
)
const runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
func isAutoStartEnabled() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
_, _, err = k.GetStringValue("u-desktop")
return err == nil
}
func setAutoStart(enabled bool) {
exePath, _ := os.Executable()
if enabled {
k, _, err := registry.CreateKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
if err != nil {
log.Println("注册表写入失败:", err)
return
}
defer k.Close()
k.SetStringValue("u-desktop", exePath)
log.Println("已开启开机启动")
} else {
k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
if err != nil {
return
}
defer k.Close()
k.DeleteValue("u-desktop")
log.Println("已关闭开机启动")
}
}
func isSystemLightTheme() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
v, _, err := k.GetIntegerValue("AppsUseLightTheme")
if err != nil {
return false
}
return v == 1
}
// settingsWndProc 拦截 WM_CLOSE隐藏窗口而非销毁
func settingsWndProc(hwnd, msg, wp, lp uintptr) uintptr {
if msg == 0x0010 { // WM_CLOSE
procShowWindow.Call(hwnd, 0) // SW_HIDE
return 0
}
r, _, _ := procCallWindowProcW.Call(settingsOldWndProc, hwnd, msg, wp, lp)
return r
}
var themeNames = []struct {
Name ThemeName
Label string
}{
{ThemeAurora, "极光"},
{ThemeStar, "星空"},
{ThemeGradient, "渐变"},
{ThemeParticle, "粒子"},
{ThemeFractal, "极光流体"},
{ThemeText, "文字"},
{ThemeAudioViz, "音频可视化"},
}
func refreshVisibleCards(cfg, oldCfg *Config) {
if oldCfg.HideWeather && !cfg.HideWeather {
go fetchAndPushWeather(getCurrentCity())
}
if oldCfg.HideZodiac && !cfg.HideZodiac {
triggerHoroscopeRefresh(cfg.Zodiac)
}
if oldCfg.HideKnowledge && !cfg.HideKnowledge {
if cfg.KnowledgeKeyword != "" {
triggerKnowledgeRefresh()
} else {
pushKnowledgePlaceholder()
}
}
if oldCfg.HideAINews && !cfg.HideAINews {
triggerAINewsRefresh()
}
}
func openSettingsWindow() {
settingsMu.Lock()
if settingsCreated {
settingsMu.Unlock()
procShowWindow.Call(settingsHwnd, 9) // SW_RESTORE
procSetForegroundWindow.Call(settingsHwnd)
settingsWv.Dispatch(func() {
settingsWv.Eval("if(window.__refreshSettings) window.__refreshSettings()")
})
return
}
settingsMu.Unlock()
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
dataDir := filepath.Join(configDir(), "settings-data")
os.MkdirAll(dataDir, 0755)
w := webview2.NewWithOptions(webview2.WebViewOptions{
AutoFocus: true,
DataPath: dataDir,
WindowOptions: webview2.WindowOptions{
Title: "桌面设置",
Width: 760,
Height: 800,
},
})
if w == nil {
log.Println("设置窗口: 创建失败")
return
}
settingsWv = w
hwnd := uintptr(w.Window())
// 子类化:拦截 WM_CLOSE → 隐藏而非销毁
settingsWndProcCb = windows.NewCallback(settingsWndProc)
oldProc, _, _ := procSetWindowLongPtrW.Call(hwnd, gwlpWndProc, settingsWndProcCb)
settingsOldWndProc = oldProc
// 初始隐藏resizeToFit 完成后再显示
procShowWindow.Call(hwnd, 0) // SW_HIDE
w.Bind("loadAllSettings", func() string {
cfg := loadConfig()
seen := map[string]bool{}
var provinces []string
citiesByProv := map[string][]map[string]string{}
for _, c := range cities {
if !seen[c.Adm1] {
seen[c.Adm1] = true
provinces = append(provinces, c.Adm1)
}
citiesByProv[c.Adm1] = append(citiesByProv[c.Adm1], map[string]string{
"id": c.ID, "name": c.Name,
})
}
type themeJSON struct {
Value string `json:"value"`
Label string `json:"label"`
}
var tl []themeJSON
for _, t := range themeNames {
tl = append(tl, themeJSON{Value: string(t.Name), Label: t.Label})
}
data, _ := json.Marshal(map[string]interface{}{
"lightTheme": isSystemLightTheme(),
"wallpaper": !cfg.HideWallpaper,
"time": !cfg.HideTime,
"weather": !cfg.HideWeather,
"zodiacCard": !cfg.HideZodiac,
"knowledgeCard": !cfg.HideKnowledge,
"layout": string(cfg.Layout),
"zodiac": cfg.Zodiac,
"city": cfg.City,
"provinces": provinces,
"citiesByProv": citiesByProv,
"wallpaperType": string(cfg.WallpaperType),
"theme": string(cfg.Theme),
"themes": tl,
"color1": cfg.Color1,
"color2": cfg.Color2,
"colorGradient": cfg.ColorGradient,
"savedColors": cfg.SavedColors,
"bingAutoRefresh": cfg.BingAutoRefresh,
"knowledgeKeyword": cfg.KnowledgeKeyword,
"knowledgePrompt": cfg.KnowledgePrompt,
"wallpaperText": cfg.WallpaperText,
"imagePath": cfg.ImagePath,
"showSeconds": cfg.ShowSeconds,
"ainewsCard": !cfg.HideAINews,
"photoDir": cfg.PhotoDir,
"photoInterval": cfg.PhotoInterval,
"photoCard": !cfg.HidePhoto,
"photoFrameMode": cfg.PhotoFrameMode,
"autoStart": isAutoStartEnabled(),
"audioVizStyle": cfg.AudioVizStyle,
"audioSensitivity": cfg.AudioSensitivity,
"audioSmoothing": cfg.AudioSmoothing,
"audioColorScheme": cfg.AudioColorScheme,
})
return string(data)
})
w.Bind("saveToggles", func(jsonStr string) string {
var data map[string]bool
if json.Unmarshal([]byte(jsonStr), &data) != nil {
return ""
}
cfg := loadConfig()
oldCfg := *cfg
if v, ok := data["wallpaper"]; ok {
cfg.HideWallpaper = !v
evalJS(fmt.Sprintf("if(window.setWallpaperVisible) setWallpaperVisible(%v)", v))
}
if v, ok := data["time"]; ok {
cfg.HideTime = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('time',%v)", v))
}
if v, ok := data["weather"]; ok {
cfg.HideWeather = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('weather',%v)", v))
}
if v, ok := data["zodiacCard"]; ok {
cfg.HideZodiac = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('zodiac',%v)", v))
}
if v, ok := data["knowledgeCard"]; ok {
cfg.HideKnowledge = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('knowledge',%v)", v))
}
if v, ok := data["ainewsCard"]; ok {
cfg.HideAINews = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('ainews',%v)", v))
}
if v, ok := data["showSeconds"]; ok {
cfg.ShowSeconds = v
evalJS(fmt.Sprintf("if(window.setShowSeconds) setShowSeconds(%v)", v))
}
if v, ok := data["autoStart"]; ok {
setAutoStart(v)
}
if v, ok := data["photoFrameMode"]; ok {
cfg.PhotoFrameMode = v
evalJS(fmt.Sprintf("if(window.setPhotoFrameMode) setPhotoFrameMode(%v)", v))
if v {
cfg.HideWallpaper = true
cfg.HideTime = true
cfg.HideWeather = true
cfg.HideZodiac = true
cfg.HideAINews = true
cfg.HideKnowledge = true
evalJS(`if(window.setWallpaperVisible) setWallpaperVisible(false)`)
for _, card := range []string{"time", "weather", "zodiac", "ainews", "knowledge"} {
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('%s',false)", card))
}
} else {
reloadWallpaper()
}
}
if v, ok := data["photoCard"]; ok {
cfg.HidePhoto = !v
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('photo',%v)", v))
if cfg.PhotoDir != "" {
if v {
restartPhotoLoop()
} else {
stopPhotoLoop()
}
}
}
saveConfig(cfg)
refreshVisibleCards(cfg, &oldCfg)
return ""
})
w.Bind("saveLayout", func(layout string) string {
cfg := loadConfig()
cfg.Layout = Layout(layout)
saveConfig(cfg)
reloadWallpaper()
return ""
})
w.Bind("saveZodiac", func(zodiac string) string {
cfg := loadConfig()
cfg.Zodiac = zodiac
saveConfig(cfg)
evalJS(fmt.Sprintf(`window.userZodiac = %q; if(window.updateTime) updateTime();`, zodiac))
triggerHoroscopeRefresh(zodiac)
return ""
})
w.Bind("saveCity", func(cityID string) string {
cfg := loadConfig()
cfg.City = cityID
saveConfig(cfg)
for _, c := range cities {
if c.ID == cityID {
go fetchAndPushWeather(c)
break
}
}
return ""
})
w.Bind("saveWallpaperType", func(wpType, theme string) string {
cfg := loadConfig()
cfg.WallpaperType = WallpaperType(wpType)
cfg.Theme = ThemeName(theme)
saveConfig(cfg)
reloadWallpaper()
return ""
})
w.Bind("pickLocalImage", func() string {
hwnd := uintptr(w.Window())
path := openFileDialog(hwnd)
if path == "" {
return ""
}
cfg := loadConfig()
cfg.WallpaperType = WPImage
cfg.ImagePath = path
saveConfig(cfg)
reloadWallpaper()
return path
})
w.Bind("enableBing", func() string {
cfg := loadConfig()
cfg.WallpaperType = WPBing
saveConfig(cfg)
reloadWallpaper()
go fetchBingHistory()
return ""
})
w.Bind("bingPrev", func() string {
bingPrev()
return bingCurrentState()
})
w.Bind("bingNext", func() string {
bingNext()
return bingCurrentState()
})
w.Bind("bingToggleFavorite", func() string {
bingToggleFavorite()
return bingCurrentState()
})
w.Bind("getBingInfo", func() string {
return bingCurrentState()
})
w.Bind("saveBingAutoRefresh", func(val bool) string {
cfg := loadConfig()
cfg.BingAutoRefresh = val
saveConfig(cfg)
return ""
})
w.Bind("getBingFavorites", func() string {
return bingFavoritesJSON()
})
w.Bind("bingSetByIdx", func(idx int) string {
return bingSetByIdx(idx)
})
w.Bind("bingThumbDataURI", func(filename string) string {
return bingThumbDataURI(filename)
})
w.Bind("pickSolidColor", func() string {
hwnd := uintptr(w.Window())
color := colorPickerDialog(hwnd, "")
if color == "" {
return ""
}
cfg := loadConfig()
cfg.WallpaperType = WPColor
cfg.Color1 = color
cfg.ColorGradient = false
saveConfig(cfg)
reloadWallpaper()
return color
})
w.Bind("pickGradientColor", func() string {
hwnd := uintptr(w.Window())
cfg := loadConfig()
c1 := colorPickerDialog(hwnd, cfg.Color1)
if c1 == "" {
return ""
}
c2 := colorPickerDialog(hwnd, cfg.Color2)
if c2 == "" {
c2 = "#16213e"
}
cfg.WallpaperType = WPColor
cfg.Color1 = c1
cfg.Color2 = c2
cfg.ColorGradient = true
saveConfig(cfg)
reloadWallpaper()
return c1 + "," + c2
})
w.Bind("addSavedColor", func(c1, c2 string, gradient bool) string {
cfg := loadConfig()
cfg.SavedColors = append(cfg.SavedColors, SavedColor{Color1: c1, Color2: c2, Gradient: gradient})
saveConfig(cfg)
return ""
})
w.Bind("removeSavedColor", func(idx int) string {
cfg := loadConfig()
if idx >= 0 && idx < len(cfg.SavedColors) {
cfg.SavedColors = append(cfg.SavedColors[:idx], cfg.SavedColors[idx+1:]...)
saveConfig(cfg)
}
return ""
})
w.Bind("applySavedColor", func(idx int) string {
cfg := loadConfig()
if idx >= 0 && idx < len(cfg.SavedColors) {
sc := cfg.SavedColors[idx]
cfg.WallpaperType = WPColor
cfg.Color1 = sc.Color1
cfg.Color2 = sc.Color2
cfg.ColorGradient = sc.Gradient
saveConfig(cfg)
reloadWallpaper()
}
return ""
})
w.Bind("saveWallpaperText", func(text string) string {
cfg := loadConfig()
cfg.WallpaperText = text
saveConfig(cfg)
reloadWallpaper()
return ""
})
w.Bind("saveKnowledgeKeyword", func(keyword string) string {
cfg := loadConfig()
cfg.KnowledgeKeyword = keyword
saveConfig(cfg)
if keyword != "" {
triggerKnowledgeRefresh()
} else {
pushKnowledgePlaceholder()
}
return ""
})
w.Bind("saveKnowledgePrompt", func(prompt string) string {
cfg := loadConfig()
cfg.KnowledgePrompt = prompt
saveConfig(cfg)
return ""
})
w.Bind("pickPhotoDir", func() string {
hwnd := uintptr(w.Window())
dir := browseForFolderDialog(hwnd)
if dir == "" {
return ""
}
cfg := loadConfig()
cfg.PhotoDir = dir
if cfg.PhotoInterval <= 0 {
cfg.PhotoInterval = 15
}
saveConfig(cfg)
restartPhotoLoop()
return dir
})
w.Bind("clearPhotoDir", func() string {
cfg := loadConfig()
cfg.PhotoDir = ""
saveConfig(cfg)
restartPhotoLoop()
return ""
})
w.Bind("savePhotoInterval", func(val int) string {
cfg := loadConfig()
cfg.PhotoInterval = val
saveConfig(cfg)
if cfg.PhotoDir != "" {
restartPhotoLoop()
}
return ""
})
w.Bind("saveAudioVizStyle", func(style string) string {
cfg := loadConfig()
cfg.AudioVizStyle = style
saveConfig(cfg)
if cfg.Theme == ThemeAudioViz {
evalJS(fmt.Sprintf(`window.__audioVizStyle=%q; if(window.setAudioVizStyle) setAudioVizStyle(%q)`, style, style))
}
return ""
})
w.Bind("saveAudioSensitivity", func(val float64) string {
cfg := loadConfig()
cfg.AudioSensitivity = val
saveConfig(cfg)
if cfg.Theme == ThemeAudioViz {
evalJS(fmt.Sprintf(`window.__audioVizSensitivity=%f; if(window.setAudioSensitivity) setAudioSensitivity(%f)`, val, val))
}
return ""
})
w.Bind("saveAudioSmoothing", func(val float64) string {
cfg := loadConfig()
cfg.AudioSmoothing = val
saveConfig(cfg)
return ""
})
w.Bind("saveAudioColorScheme", func(scheme string) string {
cfg := loadConfig()
cfg.AudioColorScheme = scheme
saveConfig(cfg)
if cfg.Theme == ThemeAudioViz {
evalJS(fmt.Sprintf(`window.__audioVizColors=%q; if(window.setAudioColorScheme) setAudioColorScheme(%q)`, scheme, scheme))
}
return ""
})
w.SetHtml(settingsHTML)
// disable resize + hide from taskbar
style, _, _ := procGetWindowLongPtrW.Call(hwnd, gwlStyle)
procSetWindowLongPtrW.Call(hwnd, gwlStyle, style & ^uintptr(wsSizebox|wsMaxbox))
procSetWindowLongPtrW.Call(hwnd, gwlExStyle, wsExToolwindow)
// resizeToFit: JS measures content, Go adjusts window frame, then shows window
w.Bind("resizeToFit", func(contentW, contentH int) string {
type rect struct{ Left, Top, Right, Bottom int32 }
var wr, cr rect
procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&wr)))
procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&cr)))
frameW := int(wr.Right-wr.Left) - int(cr.Right-cr.Left)
frameH := int(wr.Bottom-wr.Top) - int(cr.Bottom-cr.Top)
winW := contentW + frameW
winH := contentH + frameH
screenW, screenH := getScreenSize()
if winH > int(screenH)-60 {
winH = int(screenH) - 60
}
if winW > int(screenW)-60 {
winW = int(screenW) - 60
}
x := (int(screenW) - winW) / 2
y := (int(screenH) - winH) / 2
procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(winW), uintptr(winH), 1)
// 首次 resize 完成后显示窗口
procShowWindow.Call(hwnd, 9) // SW_RESTORE
procSetForegroundWindow.Call(hwnd)
return ""
})
settingsMu.Lock()
settingsHwnd = hwnd
settingsCreated = true
settingsMu.Unlock()
log.Println("设置窗口已打开")
w.Run()
log.Println("设置窗口消息循环退出")
}()
}