新增: 星座运势+AI资讯+知识卡片+桌面设置窗口+秒显示开关

- 星座运势: 天聚数行API集成,5维进度条+幸运标签+今日概述
- AI资讯: 天聚数行API,图文布局5条展示,文件缓存2小时刷新
- 知识卡片: AI生成,关键字+提示词配置,30分钟刷新
- 桌面设置: 独立WebView2窗口,760x1350,含壁纸/布局/城市/颜色等配置
- 显示控制: 壁纸/时间/天气/星座/知识/AI资讯独立开关,秒显示开关
- 文件缓存: 星座运势+AI资讯缓存到本地,启动即显示上次数据
- initDone防抖: 防止设置窗口初始化触发卡片重载
This commit is contained in:
2026-05-26 04:34:00 +08:00
parent 2287e12e0d
commit 9fd3acede3
15 changed files with 2607 additions and 384 deletions

405
settings.go Normal file
View File

@@ -0,0 +1,405 @@
package main
import (
_ "embed"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"unsafe"
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows/registry"
)
//go:embed web/settings.html
var settingsHTML string
var (
settingsMu sync.Mutex
settingsOpen bool
settingsHwnd uintptr
)
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
}
var themeNames = []struct {
Name ThemeName
Label string
}{
{ThemeAurora, "极光"},
{ThemeStar, "星空"},
{ThemeGradient, "渐变"},
{ThemeParticle, "粒子"},
{ThemeFractal, "极光流体"},
{ThemeText, "文字"},
}
func openSettingsWindow() {
settingsMu.Lock()
if settingsOpen && settingsHwnd != 0 {
settingsMu.Unlock()
procShowWindow.Call(settingsHwnd, 9)
procGetForegroundWindow.Call(settingsHwnd)
return
}
settingsOpen = true
settingsMu.Unlock()
go func() {
runtime.LockOSThread()
defer func() {
settingsMu.Lock()
settingsOpen = false
settingsHwnd = 0
settingsMu.Unlock()
runtime.UnlockOSThread()
}()
dataDir := filepath.Join(os.TempDir(), "u-desktop-settings")
os.MkdirAll(dataDir, 0755)
w := webview2.NewWithOptions(webview2.WebViewOptions{
AutoFocus: true,
DataPath: dataDir,
WindowOptions: webview2.WindowOptions{
Title: "桌面设置",
Width: 760,
Height: 1350,
},
})
if w == nil {
log.Println("设置窗口: 创建失败")
return
}
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,
})
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()
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))
}
saveConfig(cfg)
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())
c1 := colorPickerDialog(hwnd, "")
if c1 == "" {
return ""
}
c2 := colorPickerDialog(hwnd, "")
if c2 == "" {
c2 = "#16213e"
}
cfg := loadConfig()
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.SetHtml(settingsHTML)
hwnd := uintptr(w.Window())
// disable resize
style, _, _ := procGetWindowLongPtrW.Call(hwnd, uintptr(0xFFFFFFF0))
procSetWindowLongPtrW.Call(hwnd, uintptr(0xFFFFFFF0), style & ^uintptr(0x00040000|0x00010000))
// resizeToFit: JS measures content, Go adjusts window frame
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)
return ""
})
settingsMu.Lock()
settingsHwnd = hwnd
settingsMu.Unlock()
log.Println("设置窗口已打开")
w.Run()
log.Println("设置窗口已关闭")
}()
}