599 lines
16 KiB
Go
599 lines
16 KiB
Go
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("设置窗口消息循环退出")
|
||
}()
|
||
}
|