新增: 星座运势+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

165
ainews.go Normal file
View File

@@ -0,0 +1,165 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
)
const tianapiAIURL = "https://apis.tianapi.com/ai/index"
type aiNewsResp struct {
Code int `json:"code"`
Result struct {
Newslist []struct {
ID string `json:"id"`
CTime string `json:"ctime"`
Title string `json:"title"`
Description string `json:"description"`
Source string `json:"source"`
URL string `json:"url"`
PicUrl string `json:"picUrl"`
} `json:"newslist"`
} `json:"result"`
}
type aiNewsItem struct {
Title string `json:"title"`
Description string `json:"description"`
Source string `json:"source"`
CTime string `json:"ctime"`
URL string `json:"url"`
PicURL string `json:"picUrl"`
}
var (
aiNewsMu sync.Mutex
aiNewsCache []aiNewsItem
aiNewsCacheAt time.Time
)
func aiNewsCachePath() string {
return filepath.Join(configDir(), "ainews_cache.json")
}
func fetchAINews() []aiNewsItem {
aiNewsMu.Lock()
if aiNewsCache != nil && time.Since(aiNewsCacheAt) < 2*time.Hour {
cached := aiNewsCache
aiNewsMu.Unlock()
return cached
}
aiNewsMu.Unlock()
url := fmt.Sprintf("%s?key=%s", tianapiAIURL, tianapiKey)
data, err := httpGet(url)
if err != nil {
log.Println("AI资讯请求失败:", err)
return nil
}
var resp aiNewsResp
if json.Unmarshal(data, &resp) != nil || resp.Code != 200 {
log.Println("AI资讯解析失败:", string(data[:min(len(data), 100)]))
return nil
}
var items []aiNewsItem
for _, n := range resp.Result.Newslist {
items = append(items, aiNewsItem{
Title: n.Title,
Description: n.Description,
Source: n.Source,
CTime: n.CTime,
URL: n.URL,
PicURL: n.PicUrl,
})
}
aiNewsMu.Lock()
aiNewsCache = items
aiNewsCacheAt = time.Now()
aiNewsMu.Unlock()
// 缓存到文件
cacheData, _ := json.Marshal(map[string]interface{}{
"items": items,
"at": time.Now().Format(time.RFC3339),
})
if err := os.WriteFile(aiNewsCachePath(), cacheData, 0644); err != nil {
log.Println("AI资讯缓存写入失败:", err)
}
log.Printf("AI资讯已获取: %d条", len(items))
return items
}
func loadAINewsCache() []aiNewsItem {
data, err := os.ReadFile(aiNewsCachePath())
if err != nil {
return nil
}
var cached struct {
Items []aiNewsItem `json:"items"`
At string `json:"at"`
}
if json.Unmarshal(data, &cached) != nil {
return nil
}
return cached.Items
}
func pushAINews(items []aiNewsItem) {
if len(items) == 0 {
return
}
jsonData, _ := json.Marshal(items)
js := fmt.Sprintf(`if(window.updateAINewsFromGo) window.updateAINewsFromGo(%s)`, string(jsonData))
evalJS(js)
}
func aiNewsLoop() {
cfg := loadConfig()
if cfg.HideAINews {
return
}
// 先推送缓存
if cached := loadAINewsCache(); cached != nil {
pushAINews(cached)
}
time.Sleep(8 * time.Second)
cfg = loadConfig()
if cfg.HideAINews {
return
}
items := fetchAINews()
if items != nil {
pushAINews(items)
}
ticker := time.NewTicker(2 * time.Hour)
for range ticker.C {
cfg := loadConfig()
if !cfg.HideAINews {
if items := fetchAINews(); items != nil {
pushAINews(items)
}
}
}
}
func triggerAINewsRefresh() {
go func() {
if items := fetchAINews(); items != nil {
pushAINews(items)
}
}()
}

162
bing.go
View File

@@ -7,12 +7,13 @@ import (
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=8&mkt=zh-CN"
const bingAPIBase = "https://www.bing.com/HPImageArchive.aspx?format=js&n=8&mkt=zh-CN"
type bingResponse struct {
Images []struct {
@@ -67,30 +68,29 @@ func fetchBingHistory() {
bingMu.Lock()
defer bingMu.Unlock()
resp, err := httpClient.Get(bingAPI)
if err != nil {
log.Println("Bing API 请求失败:", err)
return
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return
}
var br bingResponse
if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
log.Println("Bing API 解析失败")
return
}
os.MkdirAll(bingDir(), 0755)
existing := loadBingHistory()
existingMap := make(map[string]BingRecord)
for _, r := range existing.Records {
existingMap[r.Date] = r
}
// 分页下载: idx=0,8,16... 直到命中已有记录或无新图片
for idx := 0; idx < 80; idx += 8 {
url := fmt.Sprintf("%s&idx=%d", bingAPIBase, idx)
resp, err := httpClient.Get(url)
if err != nil {
log.Printf("Bing API 请求失败 (idx=%d): %v", idx, err)
break
}
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var br bingResponse
if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
break
}
newCount := 0
for _, img := range br.Images {
date := img.StartDate
if date == "" {
@@ -107,7 +107,6 @@ func fetchBingHistory() {
imgResp, err := httpClient.Get(imgURL)
if err != nil {
log.Printf("Bing 图片下载失败 (%s): %v", date, err)
continue
}
imgData, _ := io.ReadAll(imgResp.Body)
@@ -118,10 +117,7 @@ func fetchBingHistory() {
filename := date + ".jpg"
localPath := filepath.Join(bingDir(), filename)
if err := os.WriteFile(localPath, imgData, 0644); err != nil {
log.Printf("Bing 图片保存失败 (%s): %v", date, err)
continue
}
os.WriteFile(localPath, imgData, 0644)
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
existingMap[date] = BingRecord{
@@ -130,17 +126,24 @@ func fetchBingHistory() {
Copyright: img.Copyright,
Filename: filename,
}
newCount++
}
if newCount == 0 && idx > 0 {
break
}
time.Sleep(500 * time.Millisecond)
}
// 按 API 返回顺序重建 records (newest first)
// 用 existingMap 里所有记录按 date 降序排列
var records []BingRecord
for _, img := range br.Images {
if r, ok := existingMap[img.StartDate]; ok {
for _, r := range existingMap {
records = append(records, r)
}
}
if len(records) == 0 {
return
}
sort.Slice(records, func(i, j int) bool {
return records[i].Date > records[j].Date
})
history := &BingHistory{
Records: records,
@@ -151,6 +154,7 @@ func fetchBingHistory() {
}
saveBingHistory(history)
log.Printf("Bing 壁纸: 共 %d 张", len(records))
reloadWallpaper()
}
@@ -178,12 +182,10 @@ func bingPrev() {
if len(h.Records) == 0 {
return
}
if h.CurrentIdx < len(h.Records)-1 {
h.CurrentIdx++
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
saveBingHistory(h)
log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
reloadWallpaper()
}
bingReloadImage()
}
func bingNext() {
@@ -191,12 +193,13 @@ func bingNext() {
defer bingMu.Unlock()
h := loadBingHistory()
if h.CurrentIdx > 0 {
h.CurrentIdx--
if len(h.Records) == 0 {
return
}
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
saveBingHistory(h)
log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
reloadWallpaper()
}
bingReloadImage()
}
func bingToggleFavorite() string {
@@ -229,6 +232,67 @@ func bingCopyrightInfo() string {
return ""
}
func bingCurrentState() string {
h := loadBingHistory()
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
return `{"fav":false,"label":"☆","copyright":"","idx":0,"total":0}`
}
r := h.Records[h.CurrentIdx]
label := "☆"
if r.Favorited {
label = "⭐"
}
data, _ := json.Marshal(map[string]interface{}{
"fav": r.Favorited,
"label": label,
"copyright": r.Copyright,
"date": r.Date,
"filename": r.Filename,
"idx": h.CurrentIdx,
"total": len(h.Records),
})
return string(data)
}
func bingFavoritesJSON() string {
h := loadBingHistory()
type favItem struct {
Date string `json:"date"`
Copyright string `json:"copyright"`
Filename string `json:"filename"`
Idx int `json:"idx"`
}
var favs []favItem
for i, r := range h.Records {
if r.Favorited {
favs = append(favs, favItem{r.Date, r.Copyright, r.Filename, i})
}
}
if favs == nil {
favs = []favItem{}
}
data, _ := json.Marshal(favs)
return string(data)
}
func bingSetByIdx(idx int) string {
bingMu.Lock()
defer bingMu.Unlock()
h := loadBingHistory()
if idx < 0 || idx >= len(h.Records) {
return bingCurrentState()
}
h.CurrentIdx = idx
saveBingHistory(h)
bingReloadImage()
return bingCurrentState()
}
func bingThumbDataURI(filename string) string {
p := filepath.Join(bingDir(), filename)
return imageToDataURI(p)
}
func bingWallpaperLoop() {
cfg := loadConfig()
if cfg.WallpaperType == WPBing {
@@ -240,11 +304,31 @@ func bingWallpaperLoop() {
}
}
ticker := time.NewTicker(4 * time.Hour)
// 定时切换壁纸 (1 小时间隔)
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
cfg := loadConfig()
if cfg.WallpaperType != WPBing || !cfg.BingAutoRefresh {
continue
}
h := loadBingHistory()
if len(h.Records) <= 1 {
continue
}
// 顺序切换到下一张
nextIdx := (h.CurrentIdx + 1) % len(h.Records)
h.CurrentIdx = nextIdx
saveBingHistory(h)
bingReloadImage()
log.Printf("Bing 自动切换: idx=%d/%d", nextIdx, len(h.Records))
}
// 定时拉取新壁纸 (4 小时间隔)
fetchTicker := time.NewTicker(4 * time.Hour)
for range fetchTicker.C {
cfg := loadConfig()
if cfg.WallpaperType == WPBing {
fetchBingHistory()
go fetchBingHistory()
}
}
}

View File

@@ -38,6 +38,12 @@ const (
ThemeText ThemeName = "text"
)
type SavedColor struct {
Color1 string `json:"color1"`
Color2 string `json:"color2"`
Gradient bool `json:"gradient"`
}
type Config struct {
Zodiac string `json:"zodiac"`
City string `json:"city"`
@@ -49,6 +55,17 @@ type Config struct {
ColorGradient bool `json:"colorGradient"`
WallpaperText string `json:"wallpaperText"`
Layout Layout `json:"layout"`
HideWallpaper bool `json:"hideWallpaper"`
HideTime bool `json:"hideTime"`
HideWeather bool `json:"hideWeather"`
HideZodiac bool `json:"hideZodiac"`
HideAINews bool `json:"hideAINews"`
ShowSeconds bool `json:"showSeconds"`
KnowledgeKeyword string `json:"knowledgeKeyword"`
KnowledgePrompt string `json:"knowledgePrompt"`
HideKnowledge bool `json:"hideKnowledge"`
SavedColors []SavedColor `json:"savedColors"`
BingAutoRefresh bool `json:"bingAutoRefresh"`
}
const defaultZodiac = "射手座"

11
go.mod
View File

@@ -3,19 +3,28 @@ module u-desktop
go 1.26.3
require (
github.com/getlantern/systray v1.2.2
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
golang.org/x/sys v0.45.0
modernc.org/sqlite v1.50.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/getlantern/systray v1.2.2 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

52
go.sum
View File

@@ -1,4 +1,7 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
@@ -15,22 +18,71 @@ github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sTho
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo=
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

177
horoscope.go Normal file
View File

@@ -0,0 +1,177 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
const tianapiKey = "da21ff665b09cbdcc29952a105aad97b"
const tianapiStarURL = "https://apis.tianapi.com/star/index"
var zodiacToSign = map[string]string{
"白羊座": "aries", "金牛座": "taurus", "双子座": "gemini", "巨蟹座": "cancer",
"狮子座": "leo", "处女座": "virgo", "天秤座": "libra", "天蝎座": "scorpio",
"射手座": "sagittarius", "摩羯座": "capricorn", "水瓶座": "aquarius", "双鱼座": "pisces",
}
var horoscopeMu sync.Mutex
type tianapiStarResp struct {
Code int `json:"code"`
Result struct {
List []struct {
Type string `json:"type"`
Content string `json:"content"`
} `json:"list"`
} `json:"result"`
}
type horoscopeInfo struct {
Zodiac string `json:"zodiac"`
Date string `json:"date"`
All string `json:"all"`
Love string `json:"love"`
Work string `json:"work"`
Money string `json:"money"`
Health string `json:"health"`
LuckyColor string `json:"luckyColor"`
LuckyNum string `json:"luckyNum"`
Noble string `json:"noble"`
Summary string `json:"summary"`
}
func horoscopeCachePath() string {
return filepath.Join(configDir(), "horoscope_cache.json")
}
func saveHoroscopeCache(info *horoscopeInfo) {
info.Date = time.Now().Format("2006-01-02")
data, _ := json.Marshal(info)
os.WriteFile(horoscopeCachePath(), data, 0644)
}
func loadHoroscopeCache() *horoscopeInfo {
data, err := os.ReadFile(horoscopeCachePath())
if err != nil {
return nil
}
var info horoscopeInfo
if json.Unmarshal(data, &info) != nil {
return nil
}
return &info
}
func isCacheToday(info *horoscopeInfo) bool {
return info != nil
}
func fetchHoroscope(zodiac string) *horoscopeInfo {
sign := zodiacToSign[zodiac]
if sign == "" {
sign = "sagittarius"
}
url := fmt.Sprintf("%s?key=%s&astro=%s", tianapiStarURL, tianapiKey, sign)
data, err := httpGet(url)
if err != nil {
log.Println("星座运势请求失败:", err)
return nil
}
var resp tianapiStarResp
if json.Unmarshal(data, &resp) != nil || resp.Code != 200 {
log.Println("星座运势解析失败:", string(data))
return nil
}
info := &horoscopeInfo{Zodiac: zodiac}
for _, item := range resp.Result.List {
switch item.Type {
case "综合指数":
info.All = strings.TrimSuffix(item.Content, "%")
case "爱情指数":
info.Love = strings.TrimSuffix(item.Content, "%")
case "工作指数":
info.Work = strings.TrimSuffix(item.Content, "%")
case "财运指数":
info.Money = strings.TrimSuffix(item.Content, "%")
case "健康指数":
info.Health = strings.TrimSuffix(item.Content, "%")
case "幸运颜色":
info.LuckyColor = item.Content
case "幸运数字":
info.LuckyNum = item.Content
case "贵人星座":
info.Noble = item.Content
case "今日概述":
info.Summary = strings.TrimSpace(item.Content)
}
}
log.Printf("星座运势已获取: %s", zodiac)
return info
}
func pushHoroscopeInfo(info *horoscopeInfo) {
jsonData, _ := json.Marshal(info)
log.Printf("星座运势JS: %s", string(jsonData[:min(len(jsonData), 120)]))
js := fmt.Sprintf(`if(window.updateHoroscopeFromGo) window.updateHoroscopeFromGo(%s)`, string(jsonData))
evalJS(js)
}
func pushHoroscope(zodiac string) {
info := fetchHoroscope(zodiac)
if info == nil {
log.Println("星座运势: fetchHoroscope返回nil")
return
}
saveHoroscopeCache(info)
pushHoroscopeInfo(info)
}
func horoscopeLoop() {
cfg := loadConfig()
if cfg.HideZodiac {
return
}
cached := loadHoroscopeCache()
if cached != nil {
pushHoroscopeInfo(cached)
}
time.Sleep(5 * time.Second)
cfg = loadConfig()
if cfg.HideZodiac {
return
}
today := time.Now().Format("2006-01-02")
if cached != nil && cached.Date == today && cached.Zodiac == cfg.Zodiac {
return
}
pushHoroscope(cfg.Zodiac)
ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
cfg := loadConfig()
if !cfg.HideZodiac {
pushHoroscope(cfg.Zodiac)
}
}
}
func triggerHoroscopeRefresh(zodiac string) {
go func() {
pushHoroscope(zodiac)
}()
}

220
knowledge.go Normal file
View File

@@ -0,0 +1,220 @@
package main
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"path/filepath"
"time"
_ "modernc.org/sqlite"
)
const cpaURL = "https://cpa.1216.top/v1/chat/completions"
const cpaKey = "alink-shared-key-1"
const cpaModel = "glm-4.5-air"
type knowledgeData struct {
Content string `json:"content"`
Keyword string `json:"keyword"`
}
var knowledgeDB *sql.DB
func initKnowledgeDB() {
dbPath := filepath.Join(configDir(), "knowledge.db")
var err error
knowledgeDB, err = sql.Open("sqlite", dbPath)
if err != nil {
log.Println("知识库打开失败:", err)
return
}
knowledgeDB.SetMaxOpenConns(1)
_, err = knowledgeDB.Exec(`CREATE TABLE IF NOT EXISTS knowledge_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
log.Println("知识库建表失败:", err)
}
}
func saveKnowledgeCard(keyword, content string) {
if knowledgeDB == nil {
return
}
_, err := knowledgeDB.Exec("INSERT INTO knowledge_cards (keyword, content) VALUES (?, ?)", keyword, content)
if err != nil {
log.Println("知识保存失败:", err)
}
}
func getRandomKnowledgeCard(keyword string) string {
if knowledgeDB == nil {
return ""
}
var content string
err := knowledgeDB.QueryRow(
"SELECT content FROM knowledge_cards WHERE keyword = ? ORDER BY RANDOM() LIMIT 1",
keyword,
).Scan(&content)
if err != nil {
return ""
}
return content
}
func getKnowledgeCardCount(keyword string) int {
if knowledgeDB == nil {
return 0
}
var count int
err := knowledgeDB.QueryRow(
"SELECT COUNT(*) FROM knowledge_cards WHERE keyword = ?",
keyword,
).Scan(&count)
if err != nil {
return 0
}
return count
}
func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
basePrompt := fmt.Sprintf(
"根据关键词「%s」生成一条有趣的知识小卡片。要求控制在80字以内简洁有趣有知识性。直接输出内容不要加标题、序号或其他格式。",
keyword,
)
if cfg.KnowledgePrompt != "" {
basePrompt += "\n附加要求" + cfg.KnowledgePrompt
}
body := map[string]interface{}{
"model": cpaModel,
"max_tokens": 256,
"messages": []map[string]string{
{"role": "user", "content": basePrompt},
},
}
jsonData, _ := json.Marshal(body)
req, err := http.NewRequest("POST", cpaURL, bytes.NewReader(jsonData))
if err != nil {
log.Println("知识API请求创建失败:", err)
return ""
}
req.Header.Set("Authorization", "Bearer "+cpaKey)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
log.Println("知识API请求失败:", err)
return ""
}
defer resp.Body.Close()
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if json.NewDecoder(resp.Body).Decode(&result) != nil {
log.Println("知识API响应解析失败")
return ""
}
if len(result.Choices) > 0 {
return result.Choices[0].Message.Content
}
return ""
}
func pushKnowledgeJSON(content, keyword string) {
data, _ := json.Marshal(knowledgeData{Content: content, Keyword: keyword})
evalJS(fmt.Sprintf(`if(window.updateKnowledgeFromGo) window.updateKnowledgeFromGo(%s)`, string(data)))
}
func fetchAndPushKnowledge() {
cfg := loadConfig()
keyword := cfg.KnowledgeKeyword
if keyword == "" {
return
}
var content string
count := getKnowledgeCardCount(keyword)
if count > 0 && rand.Intn(10) < 3 {
content = getRandomKnowledgeCard(keyword)
}
if content == "" {
content = fetchKnowledgeFromLLM(keyword, cfg)
if content != "" {
saveKnowledgeCard(keyword, content)
}
}
if content == "" && count > 0 {
content = getRandomKnowledgeCard(keyword)
}
if content == "" {
return
}
pushKnowledgeJSON(content, keyword)
preview := content
if len(preview) > 30 {
preview = preview[:30] + "..."
}
log.Println("知识卡片已推送:", preview)
}
func pushKnowledgeLoading(keyword string) {
pushKnowledgeJSON("加载中...", keyword)
}
func pushKnowledgePlaceholder() {
pushKnowledgeJSON("请设置知识关键字", "")
}
func knowledgeLoop() {
initKnowledgeDB()
cfg := loadConfig()
if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge {
if cached := getRandomKnowledgeCard(cfg.KnowledgeKeyword); cached != "" {
pushKnowledgeJSON(cached, cfg.KnowledgeKeyword)
} else {
pushKnowledgeLoading(cfg.KnowledgeKeyword)
}
} else if cfg.KnowledgeKeyword == "" {
pushKnowledgePlaceholder()
}
time.Sleep(3 * time.Second)
cfg = loadConfig()
if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge {
fetchAndPushKnowledge()
}
ticker := time.NewTicker(30 * time.Minute)
for range ticker.C {
cfg := loadConfig()
if cfg.KnowledgeKeyword != "" && !cfg.HideKnowledge {
fetchAndPushKnowledge()
}
}
}
func triggerKnowledgeRefresh() {
go fetchAndPushKnowledge()
}

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("设置窗口已关闭")
}()
}

View File

@@ -15,296 +15,19 @@ import (
"github.com/jchv/go-webview2"
)
var zodiacItems []*systray.MenuItem
var cityItems []*systray.MenuItem
var themeItems []*systray.MenuItem
var themeNames = []struct {
Name ThemeName
Label string
}{
{ThemeAurora, "极光"},
{ThemeStar, "星空"},
{ThemeGradient, "渐变"},
{ThemeParticle, "粒子"},
{ThemeFractal, "极光流体"},
{ThemeText, "文字"},
}
func onSystrayReady() {
systray.SetIcon(generateIcon())
systray.SetTooltip("动态壁纸引擎")
cfg := loadConfig()
mPause := systray.AddMenuItem("暂停", "暂停/继续")
systray.AddSeparator()
// 布局
mLayout := systray.AddMenuItem("布局设置", "")
mLayoutSingle := mLayout.AddSubMenuItem("合并卡片", "")
mLayoutMulti := mLayout.AddSubMenuItem("独立卡片", "")
if cfg.Layout == LayoutMulti {
mLayoutMulti.Check()
} else {
mLayoutSingle.Check()
}
systray.AddSeparator()
// 壁纸主题
mTheme := systray.AddMenuItem("壁纸主题", "")
for _, t := range themeNames {
item := mTheme.AddSubMenuItem(t.Label, t.Label)
if cfg.WallpaperType == WPTheme && cfg.Theme == t.Name {
item.Check()
}
themeItems = append(themeItems, item)
}
mLocalImage := systray.AddMenuItem("本地图片", "选择本地图片作为壁纸")
mBingMenu := systray.AddMenuItem("Bing 每日壁纸", "")
mBingEnable := mBingMenu.AddSubMenuItem("启用 Bing 壁纸", "")
mBingPrev := mBingMenu.AddSubMenuItem("◀ 上一个", "")
mBingNext := mBingMenu.AddSubMenuItem("下一个 ▶", "")
mBingFav := mBingMenu.AddSubMenuItem("★ 收藏当前壁纸", "")
mSolidColor := systray.AddMenuItem("纯色壁纸", "选择纯色壁纸")
mGradientColor := systray.AddMenuItem("渐变壁纸", "选择渐变壁纸")
systray.AddSeparator()
// 星座
mZodiac := systray.AddMenuItem("星座设置", "")
zodiacs := []string{
"白羊座", "金牛座", "双子座",
"巨蟹座", "狮子座", "处女座",
"天秤座", "天蝎座", "射手座",
"摩羯座", "水瓶座", "双鱼座",
}
for _, z := range zodiacs {
item := mZodiac.AddSubMenuItem(z, z)
if z == cfg.Zodiac {
item.Check()
}
zodiacItems = append(zodiacItems, item)
}
systray.AddSeparator()
// 城市
mCity := systray.AddMenuItem("城市设置", "")
for _, c := range cities {
item := mCity.AddSubMenuItem(c.Name, c.Adm1+" "+c.Name)
if cfg.City == c.ID {
item.Check()
}
cityItems = append(cityItems, item)
}
systray.AddSeparator()
mSettings := systray.AddMenuItem("桌面设置", "打开设置窗口")
mRestart := systray.AddMenuItem("重启", "重启程序")
mQuit := systray.AddMenuItem("退出", "退出程序")
// 布局切换
// 设置窗口
go func() {
for {
<-mLayoutSingle.ClickedCh
cfg := loadConfig()
cfg.Layout = LayoutSingle
saveConfig(cfg)
mLayoutSingle.Check()
mLayoutMulti.Uncheck()
reloadWallpaper()
}
}()
go func() {
for {
<-mLayoutMulti.ClickedCh
cfg := loadConfig()
cfg.Layout = LayoutMulti
saveConfig(cfg)
mLayoutSingle.Uncheck()
mLayoutMulti.Check()
reloadWallpaper()
}
}()
// 主题切换监听
for i, item := range themeItems {
go func(idx int, mi *systray.MenuItem) {
for {
<-mi.ClickedCh
cfg := loadConfig()
cfg.WallpaperType = WPTheme
cfg.Theme = themeNames[idx].Name
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
mi.Check()
log.Printf("主题切换: %s", themeNames[idx].Label)
reloadWallpaper()
}
}(i, item)
}
// 本地图片
go func() {
for {
<-mLocalImage.ClickedCh
path := openFileDialog(wvHwnd)
if path == "" {
continue
}
cfg := loadConfig()
cfg.WallpaperType = WPImage
cfg.ImagePath = path
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
log.Printf("本地图片: %s", path)
reloadWallpaper()
}
}()
// Bing 启用
go func() {
for {
<-mBingEnable.ClickedCh
cfg := loadConfig()
cfg.WallpaperType = WPBing
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
log.Println("切换 Bing 壁纸")
go fetchBingHistory()
}
}()
// Bing 上一个
go func() {
for {
<-mBingPrev.ClickedCh
bingPrev()
}
}()
// Bing 下一个
go func() {
for {
<-mBingNext.ClickedCh
bingNext()
}
}()
// Bing 收藏
go func() {
for {
<-mBingFav.ClickedCh
title := bingToggleFavorite()
if title != "" {
mBingFav.SetTitle(title)
}
}
}()
// 纯色壁纸
go func() {
for {
<-mSolidColor.ClickedCh
color := colorPickerDialog(wvHwnd, "")
if color == "" {
continue
}
cfg := loadConfig()
cfg.WallpaperType = WPColor
cfg.Color1 = color
cfg.ColorGradient = false
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
log.Printf("纯色壁纸: %s", color)
reloadWallpaper()
}
}()
// 渐变壁纸
go func() {
for {
<-mGradientColor.ClickedCh
c1 := colorPickerDialog(wvHwnd, "")
if c1 == "" {
continue
}
c2 := colorPickerDialog(wvHwnd, "")
if c2 == "" {
c2 = "#16213e"
}
cfg := loadConfig()
cfg.WallpaperType = WPColor
cfg.Color1 = c1
cfg.Color2 = c2
cfg.ColorGradient = true
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
log.Printf("渐变壁纸: %s -> %s", c1, c2)
reloadWallpaper()
}
}()
// 星座选择监听
for i, item := range zodiacItems {
go func(idx int, mi *systray.MenuItem) {
name := zodiacs[idx]
for {
<-mi.ClickedCh
cfg := loadConfig()
cfg.Zodiac = name
saveConfig(cfg)
for _, it := range zodiacItems {
it.Uncheck()
}
mi.Check()
evalJS(fmt.Sprintf(`window.userZodiac = %q; if(window.updateTime) updateTime();`, name))
}
}(i, item)
}
// 城市选择监听
for i, item := range cityItems {
go func(idx int, mi *systray.MenuItem) {
for {
<-mi.ClickedCh
city := cities[idx]
cfg := loadConfig()
cfg.City = city.ID
saveConfig(cfg)
for _, it := range cityItems {
it.Uncheck()
}
mi.Check()
go fetchAndPushWeather(city)
}
}(i, item)
}
// 暂停
go func() {
for {
<-mPause.ClickedCh
newVal := 1 - atomic.LoadInt32(&paused)
atomic.StoreInt32(&paused, newVal)
isPaused := newVal == 1
if isPaused {
mPause.SetTitle("继续")
} else {
mPause.SetTitle("暂停")
}
evalJS("if(window.setPaused) setPaused(" + strconv.FormatBool(isPaused) + ")")
<-mSettings.ClickedCh
openSettingsWindow()
}
}()
@@ -326,7 +49,10 @@ func onSystrayReady() {
go startWebView()
go weatherLoop()
go horoscopeLoop()
go aiNewsLoop()
go bingWallpaperLoop()
go knowledgeLoop()
}
func startWebView() {

View File

@@ -78,9 +78,39 @@ func buildWallpaperHTML(cfg *Config) string {
if bg == "" {
bg = themeAurora
}
html := strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1)
bgWrapped := fmt.Sprintf(`<div id="bg-layer">%s</div>`, bg)
if cfg.HideWallpaper {
bgWrapped = `<div id="bg-layer" style="display:none"></div>`
}
html := strings.Replace(overlayHTML, "{{BACKGROUND}}", bgWrapped, 1)
html = strings.Replace(html, "{{LAYOUT}}", string(cfg.Layout), 1)
var bodyClasses []string
if cfg.HideTime {
bodyClasses = append(bodyClasses, "hide-time")
}
if cfg.HideWeather {
bodyClasses = append(bodyClasses, "hide-weather")
}
if cfg.HideZodiac {
bodyClasses = append(bodyClasses, "hide-zodiac")
}
showSec := "false"
if cfg.ShowSeconds {
showSec = "true"
}
html = strings.Replace(html, "{{SHOW_SECONDS}}", showSec, 1)
if cfg.HideAINews {
bodyClasses = append(bodyClasses, "hide-ainews")
}
if cfg.HideKnowledge {
bodyClasses = append(bodyClasses, "hide-knowledge")
}
if len(bodyClasses) > 0 {
cls := strings.Join(bodyClasses, " ")
html = strings.Replace(html, `layout-`+string(cfg.Layout), `layout-`+string(cfg.Layout)+" "+cls, 1)
}
// 注入自定义文字
if cfg.WallpaperType == WPTheme && cfg.Theme == ThemeText && cfg.WallpaperText != "" {
escaped := strings.ReplaceAll(cfg.WallpaperText, `\`, `\\`)
@@ -133,3 +163,18 @@ func reloadWallpaper() {
go fetchAndPushWeather(city)
}()
}
func bingReloadImage() {
if wv == nil || wvHwnd == 0 {
return
}
p := getCurrentBingPath()
if p == "" {
return
}
src := imageToDataURI(p)
if src == "" {
return
}
evalJS(fmt.Sprintf(`var bg=document.querySelector('#bg-layer img'); if(bg) bg.src=%q;`, src))
}

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"time"
)
@@ -34,6 +35,12 @@ var cities = []City{
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
{"101250101", "长沙", "长沙", "湖南"},
{"101270101", "成都", "成都", "四川"},
{"101090101", "石家庄", "石家庄", "河北"},
{"101090206", "任丘", "任丘", "河北"},
{"101090301", "邯郸", "邯郸", "河北"},
{"101290106", "宣威", "宣威", "云南"},
{"101290101", "昆明", "昆明", "云南"},
{"101260101", "贵阳", "贵阳", "贵州"},
}
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
@@ -174,7 +181,29 @@ type currentWeather struct {
Temp string `json:"temp"`
}
var (
weatherCacheMu sync.Mutex
weatherCache map[string]time.Time
weatherCacheData map[string]string
)
func init() {
weatherCache = make(map[string]time.Time)
weatherCacheData = make(map[string]string)
}
func fetchAndPushWeather(city City) {
// 5 分钟内同一城市不重复请求
weatherCacheMu.Lock()
if last, ok := weatherCache[city.ID]; ok && time.Since(last) < 5*time.Minute {
if cached, ok := weatherCacheData[city.ID]; ok {
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, cached))
return
}
}
weatherCacheMu.Unlock()
type weatherData struct {
Current string `json:"current"`
Hourly []hourlyItem `json:"hourly"`
@@ -200,6 +229,10 @@ func fetchAndPushWeather(city City) {
wd.Daily = fetchDailyForecast(city.ID)
jsonData, _ := json.Marshal(wd)
weatherCacheMu.Lock()
weatherCache[city.ID] = time.Now()
weatherCacheData[city.ID] = string(jsonData)
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
log.Println("天气已推送:", wd.Current)
}

View File

@@ -22,7 +22,6 @@ html, body {
border-radius: 20px;
padding: 24px 28px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255,255,255,0.08);
z-index: 10;
}
.time {
@@ -34,6 +33,20 @@ html, body {
font-variant-numeric: tabular-nums;
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
}
@keyframes hourlyGlow {
0% { text-shadow: 0 0 30px rgba(255,255,255,1), 0 0 80px rgba(180,200,255,0.8), 0 0 120px rgba(100,150,255,0.5); color: #fff; }
8% { text-shadow: 0 0 50px rgba(255,255,255,1), 0 0 100px rgba(180,200,255,1), 0 0 160px rgba(100,150,255,0.6); }
18% { text-shadow: 0 0 20px rgba(255,255,255,0.6), 0 0 40px rgba(180,200,255,0.3); }
28% { text-shadow: 0 0 35px rgba(255,255,255,0.85), 0 0 70px rgba(180,200,255,0.6), 0 0 100px rgba(100,150,255,0.35); }
38% { text-shadow: 0 0 15px rgba(255,255,255,0.4), 0 0 30px rgba(180,200,255,0.2); }
48% { text-shadow: 0 0 25px rgba(255,255,255,0.6), 0 0 50px rgba(180,200,255,0.4); }
60% { text-shadow: 0 0 10px rgba(255,255,255,0.25), 0 0 20px rgba(180,200,255,0.12); }
75% { text-shadow: 0 0 15px rgba(255,255,255,0.35), 0 0 30px rgba(180,200,255,0.2); }
100% { text-shadow: 0 2px 20px rgba(0,0,0,0.5); color: #fff; }
}
.time.hourly-glow {
animation: hourlyGlow 6s ease-out forwards;
}
.date {
font-size: 14px;
color: rgba(255,255,255,0.7);
@@ -101,6 +114,152 @@ html, body {
line-height: 1.6;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.zodiac-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 2px;
}
.zodiac-date {
font-size: 11px;
opacity: 0.4;
margin-bottom: 14px;
}
.zodiac-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 7px;
font-size: 12px;
}
.zodiac-bar-label {
width: 28px;
opacity: 0.55;
flex-shrink: 0;
}
.zodiac-bar-track {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.08);
border-radius: 2px;
overflow: hidden;
}
.zodiac-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease;
}
.zodiac-bar-val {
width: 30px;
text-align: right;
font-size: 11px;
opacity: 0.7;
flex-shrink: 0;
}
.zodiac-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin: 12px 0;
}
.zodiac-tag {
font-size: 10px;
background: rgba(255,255,255,0.06);
padding: 2px 8px;
border-radius: 5px;
opacity: 0.6;
}
.zodiac-summary {
font-size: 12px;
opacity: 0.6;
line-height: 1.7;
}
/* ===== AI 资讯 ===== */
.ainews-header {
font-size: 11px;
font-weight: 500;
color: rgba(255,255,255,0.45);
letter-spacing: 1px;
margin-bottom: 10px;
}
.ainews-item {
display: flex;
gap: 12px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.ainews-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.ainews-img {
width: 80px;
height: 54px;
border-radius: 6px;
object-fit: cover;
flex-shrink: 0;
opacity: 0.85;
}
.ainews-body {
flex: 1;
min-width: 0;
}
.ainews-title-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.ainews-title {
font-size: 13px;
color: rgba(255,255,255,0.9);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.ainews-source {
font-size: 10px;
opacity: 0.4;
flex-shrink: 0;
}
.ainews-desc {
font-size: 11px;
opacity: 0.5;
line-height: 1.5;
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===== 知识卡片 ===== */
.knowledge-header {
font-size: 11px;
color: rgba(255,255,255,0.45);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
letter-spacing: 0.5px;
}
.knowledge-keyword-tag {
background: rgba(255,255,255,0.08);
padding: 1px 8px;
border-radius: 4px;
font-size: 10px;
color: rgba(255,255,255,0.6);
}
.knowledge-content {
font-size: 14px;
color: rgba(255,255,255,0.9);
line-height: 1.7;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.divider {
height: 1px;
@@ -131,7 +290,8 @@ body.layout-single #info {
top: 40px;
right: 40px;
text-align: left;
min-width: 320px;
width: calc(50vw - 60px);
z-index: 10;
}
body.layout-single #info .date {
text-align: left;
@@ -147,20 +307,65 @@ body.layout-multi #card-time {
right: 40px;
text-align: left;
min-width: 280px;
z-index: 10;
}
body.layout-multi #card-weather {
body.layout-multi #card-bottom {
position: fixed;
bottom: 80px;
right: 40px;
text-align: right;
min-width: 420px;
width: calc(50vw - 60px);
display: flex;
gap: 16px;
align-items: flex-end;
z-index: 10;
}
body.layout-multi #card-zodiac {
body.layout-multi #card-right-col {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
min-width: 0;
}
body.layout-multi #card-right-col #card-knowledge {
text-align: left;
}
body.layout-multi #card-right-col #card-ainews {
text-align: left;
}
body.layout-multi #card-bottom #card-weather {
text-align: right;
}
body.layout-multi #card-ainews {
position: fixed;
bottom: 80px;
left: 40px;
min-width: 200px;
width: calc(50vw - 80px);
z-index: 10;
}
body.layout-multi #card-zodiac {
position: fixed;
top: 200px;
right: 40px;
width: 280px;
z-index: 10;
}
/* ===== 卡片隐藏 ===== */
body.hide-time #card-time,
body.hide-time #info .time,
body.hide-time #info .date { display: none !important; }
body.hide-weather #card-weather,
body.hide-weather #info .weather-section,
body.hide-weather #info .current-weather,
body.hide-weather #info .forecast-title,
body.hide-weather #info .weather-forecast,
body.hide-weather #info .daily-forecast { display: none !important; }
body.hide-zodiac #card-zodiac,
body.hide-zodiac #info .zodiac-text { display: none !important; }
body.hide-ainews #card-ainews,
body.hide-ainews #info .ainews-section { display: none !important; }
body.hide-knowledge #card-knowledge,
body.hide-knowledge #info .knowledge-section { display: none !important; }
</style>
</head>
<body class="layout-{{LAYOUT}}">
@@ -172,6 +377,16 @@ body.layout-multi #card-zodiac {
<div class="date" id="date">1月1日 周一</div>
<div class="time" id="time">00:00</div>
<div class="divider"></div>
<div class="ainews-section">
<div class="ainews-header">🤖 AI 资讯</div>
<div id="ainews">加载中...</div>
</div>
<div class="divider"></div>
<div class="knowledge-section">
<div class="knowledge-header">💡 知识卡片 <span class="knowledge-keyword-tag" id="knowledgeTag"></span></div>
<div class="knowledge-content" id="knowledge">请设置知识关键字</div>
</div>
<div class="divider"></div>
<div>
<div class="current-weather" id="currentWeather">加载中...</div>
<div class="forecast-title">24小时预报</div>
@@ -190,6 +405,19 @@ body.layout-multi #card-zodiac {
<div class="date" id="date2">1月1日 周一</div>
<div class="time" id="time2">00:00</div>
</div>
<div id="card-zodiac" class="card">
<div class="zodiac-text" id="zodiac2">加载中...</div>
</div>
<div id="card-ainews" class="card">
<div class="ainews-header">🤖 AI 资讯</div>
<div id="ainews2">加载中...</div>
</div>
<div id="card-bottom">
<div id="card-right-col">
<div id="card-knowledge" class="card">
<div class="knowledge-header">💡 知识卡片 <span class="knowledge-keyword-tag" id="knowledgeTag2"></span></div>
<div class="knowledge-content" id="knowledge2">请设置知识关键字</div>
</div>
<div id="card-weather" class="card">
<div class="current-weather" id="currentWeather2">加载中...</div>
<div class="forecast-title">24小时预报</div>
@@ -197,8 +425,7 @@ body.layout-multi #card-zodiac {
<div class="forecast-title" style="margin-top:12px">7日预报</div>
<div class="daily-forecast" id="dailyForecast2"></div>
</div>
<div id="card-zodiac" class="card">
<div class="zodiac-text" id="zodiac2">加载中...</div>
</div>
</div>
</div>
@@ -206,22 +433,25 @@ body.layout-multi #card-zodiac {
<script>
var lastTimeStr='', lastDateStr='', lastZodiac='';
var horoscopeInfo=null;
var zodiacData = {
'白羊座':{icon:'♈',date:'3.21-4.19',fortune:'今日运势旺盛,适合开展新计划。'},
'金牛座':{icon:'♉',date:'4.20-5.20',fortune:'财运不错,但需注意健康。'},
'双子座':{icon:'♊',date:'5.21-6.21',fortune:'人际关系活跃,社交运势佳。'},
'巨蟹座':{icon:'♋',date:'6.22-7.22',fortune:'情绪敏感,适合独处思考。'},
'狮子座':{icon:'♌',date:'7.23-8.22',fortune:'自信爆棚,工作表现突出。'},
'处女座':{icon:'♍',date:'8.23-9.22',fortune:'细节决定成败,专注当下。'},
'天秤座':{icon:'♎',date:'9.23-10.23',fortune:'感情运佳,单身者有机会。'},
'天蝎座':{icon:'♏',date:'10.24-11.22',fortune:'直觉敏锐,适合做决策。'},
'射手座':{icon:'♐',date:'11.23-12.21',fortune:'冒险精神旺盛,出行注意安全。'},
'摩羯座':{icon:'♑',date:'12.22-1.19',fortune:'事业运佳,工作效率高。'},
'水瓶座':{icon:'♒',date:'1.20-2.18',fortune:'创新思维活跃,灵感不断。'},
'双鱼座':{icon:'♓',date:'2.19-3.20',fortune:'艺术灵感丰富,适合创作。'}
'白羊座':{icon:'♈',date:'3.21-4.19'},
'金牛座':{icon:'♉',date:'4.20-5.20'},
'双子座':{icon:'♊',date:'5.21-6.21'},
'巨蟹座':{icon:'♋',date:'6.22-7.22'},
'狮子座':{icon:'♌',date:'7.23-8.22'},
'处女座':{icon:'♍',date:'8.23-9.22'},
'天秤座':{icon:'♎',date:'9.23-10.23'},
'天蝎座':{icon:'♏',date:'10.24-11.22'},
'射手座':{icon:'♐',date:'11.23-12.21'},
'摩羯座':{icon:'♑',date:'12.22-1.19'},
'水瓶座':{icon:'♒',date:'1.20-2.18'},
'双鱼座':{icon:'♓',date:'2.19-3.20'}
};
var barColors={all:'#e0e0e0',love:'#ff6b9d',work:'#4fc3f7',money:'#ffd54f',health:'#81c784'};
function getUserZodiac(){ return window.userZodiac||'射手座'; }
function setEl(id,html){
@@ -233,14 +463,52 @@ function setText(id,txt){
if(e) e.textContent=txt;
}
function buildBar(label,val,color){
var v=parseInt(val)||0;
return '<div class="zodiac-bar">'+
'<span class="zodiac-bar-label">'+label+'</span>'+
'<div class="zodiac-bar-track"><div class="zodiac-bar-fill" style="width:'+v+'%;background:'+color+'"></div></div>'+
'<span class="zodiac-bar-val">'+val+'%</span></div>';
}
function buildZodiacHTML(name){
var z=zodiacData[name]||{icon:'✨',date:''};
var html='<div class="zodiac-title">'+z.icon+' '+name+'运势</div>';
html+='<div class="zodiac-date">'+z.date+'</div>';
if(horoscopeInfo&&horoscopeInfo.zodiac===name){
html+=buildBar('综合',horoscopeInfo.all,barColors.all);
html+=buildBar('爱情',horoscopeInfo.love,barColors.love);
html+=buildBar('工作',horoscopeInfo.work,barColors.work);
html+=buildBar('财运',horoscopeInfo.money,barColors.money);
html+=buildBar('健康',horoscopeInfo.health,barColors.health);
html+='<div class="zodiac-tags">';
if(horoscopeInfo.luckyColor) html+='<span class="zodiac-tag">🎨 '+horoscopeInfo.luckyColor+'</span>';
if(horoscopeInfo.luckyNum) html+='<span class="zodiac-tag">🔢 '+horoscopeInfo.luckyNum+'</span>';
if(horoscopeInfo.noble) html+='<span class="zodiac-tag">⭐ '+horoscopeInfo.noble+'</span>';
html+='</div>';
if(horoscopeInfo.summary) html+='<div class="zodiac-summary">'+horoscopeInfo.summary+'</div>';
} else {
html+='<div style="opacity:0.4;font-size:12px;margin-top:8px">运势加载中...</div>';
}
return html;
}
function syncZodiacHeight(){
var zw=document.getElementById('card-zodiac');
var ww=document.getElementById('card-weather');
if(!zw||!ww||!document.body.classList.contains('layout-multi')) return;
var wh=ww.getBoundingClientRect().height;
zw.style.minHeight=wh+'px';
}
function updateZodiacDisplay(){
var name=getUserZodiac();
if(name===lastZodiac) return;
lastZodiac=name;
var z=zodiacData[name]||{icon:'✨',date:'',fortune:'运势平稳,保持平常心。'};
var html=z.icon+' '+name+'运势 <span style="opacity:0.4;font-size:12px">'+z.date+'</span><br><span style="opacity:0.6;font-size:12px">'+z.fortune+'</span>';
var html=buildZodiacHTML(name);
setEl('zodiac',html);
setEl('zodiac2',html);
syncZodiacHeight();
}
var holidays=[
@@ -266,12 +534,29 @@ function getNextHoliday(now){
var target=new Date(y,h.m-1,h.d);
var diff=Math.ceil((target-now)/(1000*60*60*24));
if(diff>0&&diff<=60) results.push({diff:diff,name:h.name});
if(diff<0){target=new Date(y+1,h.m-1,h.d);diff=Math.ceil((target-now)/(1000*60*60*24));if(diff>0&&diff<=60)results.push({diff:diff,name:h.name});}
if(diff<0){
target=new Date(y+1,h.m-1,h.d);
diff=Math.ceil((target-now)/(1000*60*60*24));
if(diff>0&&diff<=60) results.push({diff:diff,name:h.name});
}
}
results.sort(function(a,b){return a.diff-b.diff;});
return results.length>0?'距'+results[0].name+'还有'+results[0].diff+'天':'';
}
window.setCardVisible=function(card,visible){
if(visible){document.body.classList.remove('hide-'+card);}
else{document.body.classList.add('hide-'+card);}
};
window.setWallpaperVisible=function(visible){
var bg=document.getElementById('bg-layer');
if(bg){bg.style.display=visible?'':'none';}
};
window._showSeconds={{SHOW_SECONDS}};
window.setShowSeconds=function(v){window._showSeconds=v; updateTime();};
function updateTime(){
var now=new Date();
var hh=String(now.getHours()).padStart(2,'0');
@@ -280,10 +565,21 @@ function updateTime(){
var month=now.getMonth()+1;
var day=now.getDate();
var week=['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
var timeStr=hh+':'+mm+':'+ss;
var timeStr=window._showSeconds?(hh+':'+mm+':'+ss):(hh+':'+mm);
var dateStr=month+'月'+day+'日 '+week;
if(timeStr!==lastTimeStr){
setText('time',timeStr); setText('time2',timeStr); lastTimeStr=timeStr;
if(mm==='00'&&ss==='00'){
['time','time2'].forEach(function(id){
var el=document.getElementById(id);
if(el){
el.classList.remove('hourly-glow');
void el.offsetWidth;
el.classList.add('hourly-glow');
el.addEventListener('animationend',function(){ el.classList.remove('hourly-glow'); },{once:true});
}
});
}
}
if(dateStr!==lastDateStr){
var holiday=getNextHoliday(now);
@@ -293,6 +589,50 @@ function updateTime(){
updateZodiacDisplay();
}
window.updateHoroscopeFromGo=function(data){
console.log('[horoscope] received:', typeof data, JSON.stringify(data).substring(0,100));
if(typeof data==='string') data=JSON.parse(data);
horoscopeInfo=data;
lastZodiac='';
window.userZodiac=data.zodiac;
updateZodiacDisplay();
syncZodiacHeight();
};
window.updateAINewsFromGo=function(items){
if(typeof items==='string') items=JSON.parse(items);
if(!items||!items.length) return;
var html='';
var count=Math.min(items.length,5);
for(var i=0;i<count;i++){
var n=items[i];
var time=n.ctime||'';
if(time.length>10) time=time.substring(5,10);
html+='<div class="ainews-item">';
if(n.picUrl){
html+='<img class="ainews-img" src="'+n.picUrl+'" loading="lazy" onerror="this.style.display=\'none\'">';
}
html+='<div class="ainews-body">';
html+='<div class="ainews-title-row"><span class="ainews-title">'+n.title+'</span><span class="ainews-source">'+n.source+' · '+time+'</span></div>';
if(n.description) html+='<div class="ainews-desc">'+n.description+'</div>';
html+='</div></div>';
}
setEl('ainews',html);
setEl('ainews2',html);
};
window.updateKnowledgeFromGo=function(data){
if(typeof data==='string') data=JSON.parse(data);
var ids=['knowledge','knowledge2'];
var tagIds=['knowledgeTag','knowledgeTag2'];
ids.forEach(function(id,i){
var el=document.getElementById(id);
if(el) el.textContent=data.content||'';
var tag=document.getElementById(tagIds[i]);
if(tag) tag.textContent=data.keyword?'#'+data.keyword:'';
});
};
window.updateWeatherFromGo=function(data){
if(typeof data==='string') data=JSON.parse(data);
@@ -318,6 +658,7 @@ window.updateWeatherFromGo=function(data){
renderWeather('currentWeather','hourlyForecast','dailyForecast');
renderWeather('currentWeather2','hourlyForecast2','dailyForecast2');
syncZodiacHeight();
};
updateTime();

699
web/settings.html Normal file
View File

@@ -0,0 +1,699 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg: #0f0f1a;
--card-bg: rgba(255,255,255,0.04);
--card-border: rgba(255,255,255,0.06);
--card-divider: rgba(255,255,255,0.04);
--text: #e0e0e0;
--text-strong: #fff;
--text-weak: rgba(255,255,255,0.25);
--text-muted: rgba(255,255,255,0.5);
--input-bg: rgba(255,255,255,0.08);
--input-border: rgba(255,255,255,0.1);
--input-border-focus: #4f8cff;
--accent: #4f8cff;
--toggle-track: rgba(255,255,255,0.1);
--toggle-thumb: rgba(255,255,255,0.5);
--option-bg: #1a1a2e;
--footer-color: rgba(255,255,255,0.3);
}
.light {
--bg: #f5f5f5;
--card-bg: rgba(0,0,0,0.03);
--card-border: rgba(0,0,0,0.08);
--card-divider: rgba(0,0,0,0.05);
--text: #333;
--text-strong: #111;
--text-weak: rgba(0,0,0,0.35);
--text-muted: rgba(0,0,0,0.55);
--input-bg: rgba(0,0,0,0.04);
--input-border: rgba(0,0,0,0.12);
--input-border-focus: #2b6cb0;
--accent: #2b6cb0;
--toggle-track: rgba(0,0,0,0.12);
--toggle-thumb: rgba(0,0,0,0.45);
--option-bg: #fff;
--footer-color: rgba(0,0,0,0.35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
padding: 16px 18px;
user-select: none;
-webkit-font-smoothing: antialiased;
overflow-y: auto;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
.light ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); }
.light ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.22); }
.header { margin-bottom: 14px; }
.header h1 { font-size: 16px; font-weight: 600; color: var(--text-strong); }
.header p { font-size: 11px; color: var(--text-weak); margin-top: 2px; }
.section { margin-bottom: 12px; }
.section-label {
font-size: 10px; font-weight: 600; color: var(--text-weak);
text-transform: uppercase; letter-spacing: 1.5px;
margin-bottom: 4px; padding-left: 2px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
}
.item {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 14px;
}
.item + .item { border-top: 1px solid var(--card-divider); }
.item-label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
.item-desc { font-size: 10px; color: var(--text-weak); margin-top: 1px; }
.item-sub { padding-left: 32px; }
/* Toggle */
.switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.switch input { display: none; }
.switch .track {
position: absolute; inset: 0;
background: var(--toggle-track);
border-radius: 10px; cursor: pointer; transition: background 0.2s;
}
.switch .thumb {
position: absolute; width: 14px; height: 14px; top: 3px; left: 3px;
background: var(--toggle-thumb);
border-radius: 50%; transition: all 0.2s; pointer-events: none;
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track .thumb { transform: translateX(16px); background: var(--text-strong); }
/* Select */
select {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 3px 6px;
font-family: inherit; cursor: pointer; outline: none;
min-width: 80px; max-width: 160px;
}
select:focus { border-color: var(--input-border-focus); }
select option { background: var(--option-bg); color: var(--text); }
/* Button */
.btn {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 10px;
font-family: inherit; cursor: pointer; transition: background 0.15s; white-space: nowrap;
}
.btn:hover { background: var(--card-border); }
.btn:active { background: var(--input-bg); }
.btn.active { background: var(--accent); border-color: var(--accent); color: var(--text-strong); }
.btn-sm { padding: 3px 8px; font-size: 10px; }
.btn-group { display: flex; gap: 4px; }
.radio-tabs { display: flex; flex-wrap: wrap; gap: 4px; }
.bing-nav { display: flex; gap: 4px; align-items: center; }
.wp-type-section { display: none; }
.wp-type-section.visible { display: block; }
input[type="text"] {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 8px;
font-family: inherit; outline: none; width: 140px;
}
input[type="text"]:focus { border-color: var(--input-border-focus); }
/* City Picker */
.city-picker {
position: relative;
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; padding: 4px 24px 4px 8px; cursor: pointer;
font-size: 11px; min-width: 140px; max-width: 180px;
}
.city-picker:focus, .city-picker.open { border-color: var(--input-border-focus); }
.city-picker-text { color: var(--text); }
.city-picker-arrow {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
color: var(--text-weak); font-size: 10px; pointer-events: none;
}
.city-panel {
display: none; position: absolute; right: -1px; bottom: calc(100% + 4px);
background: var(--option-bg); border: 1px solid var(--input-border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);
z-index: 1000; width: 260px; height: 200px; overflow: hidden;
}
.city-picker.open .city-panel { display: flex; }
.city-col {
flex: 1; overflow-y: auto; border-right: 1px solid var(--card-divider);
}
.city-col:last-child { border-right: none; }
.city-col div {
padding: 5px 10px; font-size: 11px; color: var(--text); cursor: pointer;
white-space: nowrap;
}
.city-col div:hover { background: var(--input-bg); }
.city-col div.active { color: var(--accent); font-weight: 600; }
/* Saved colors */
.color-swatch {
width: 28px; height: 28px; border-radius: 6px; cursor: pointer;
border: 2px solid transparent; transition: border-color 0.15s; position: relative;
}
.color-swatch:hover { border-color: var(--accent); }
.color-swatch .del {
display: none; position: absolute; top: -6px; right: -6px;
width: 14px; height: 14px; border-radius: 50%;
background: #e53e3e; color: #fff; font-size: 9px; line-height: 14px;
text-align: center; cursor: pointer;
}
.color-swatch:hover .del { display: block; }
.footer {
text-align: center; font-size: 11px; color: var(--footer-color); margin-top: 12px;
padding: 6px 0; letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="header">
<h1>桌面设置</h1>
<p>壁纸 · 布局 · 信息显示</p>
</div>
<!-- 显示控制 -->
<div class="section">
<div class="section-label">显示控制</div>
<div class="card">
<div class="item">
<div><div class="item-label">显示壁纸</div></div>
<label class="switch"><input type="checkbox" id="wallpaper" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">时间日期</div></div>
<label class="switch"><input type="checkbox" id="time" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item item-sub">
<div><div class="item-label">显示秒</div></div>
<label class="switch"><input type="checkbox" id="showSeconds" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">天气信息</div></div>
<label class="switch"><input type="checkbox" id="weather" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">星座运势</div></div>
<label class="switch"><input type="checkbox" id="zodiacCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">知识卡片</div></div>
<label class="switch"><input type="checkbox" id="knowledgeCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">AI 资讯</div></div>
<label class="switch"><input type="checkbox" id="ainewsCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
</div>
</div>
<!-- 壁纸类型 -->
<div class="section">
<div class="section-label">壁纸类型</div>
<div class="card">
<div class="item">
<div class="item-label">类型选择</div>
<div class="radio-tabs" id="wpTypeTabs">
<button class="btn" data-type="theme">主题</button>
<button class="btn" data-type="image">本地图片</button>
<button class="btn" data-type="bing">Bing</button>
<button class="btn" data-type="color">纯色/渐变</button>
</div>
</div>
</div>
</div>
<!-- 主题选择 -->
<div class="section wp-type-section" id="sec-theme">
<div class="section-label">壁纸主题</div>
<div class="card">
<div class="item">
<div class="item-label">选择主题</div>
<select id="themeSelect"></select>
</div>
<div class="item" id="textInputRow" style="display:none">
<div class="item-label">自定义文字</div>
<input type="text" id="wallpaperText" placeholder="输入显示文字">
</div>
</div>
</div>
<!-- 本地图片 -->
<div class="section wp-type-section" id="sec-image">
<div class="section-label">本地图片</div>
<div class="card">
<div class="item">
<div class="item-desc" id="imagePathDisplay">未选择图片</div>
<button class="btn" id="btnPickImage">选择图片</button>
</div>
</div>
</div>
<!-- Bing -->
<div class="section wp-type-section" id="sec-bing">
<div class="section-label">Bing 每日壁纸</div>
<div class="card">
<div class="item">
<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0">
<img id="bingPreview" style="width:64px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0;display:none">
<div style="min-width:0;overflow:hidden">
<div class="item-label" id="bingCopyright" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Bing 每日壁纸</div>
<div class="item-desc" id="bingIdx"></div>
</div>
</div>
<div class="bing-nav" style="flex-shrink:0">
<button class="btn btn-sm" id="btnBingPrev"></button>
<button class="btn btn-sm" id="btnBingNext"></button>
<button class="btn btn-sm" id="btnBingFav"></button>
</div>
</div>
<div class="item">
<div><div class="item-label">定时切换</div><div class="item-desc">每小时自动切换壁纸</div></div>
<label class="switch"><input type="checkbox" id="bingAutoRefresh"><span class="track"><span class="thumb"></span></span></label>
</div>
</div>
<div id="bingFavSection" style="display:none;margin-top:6px">
<div class="section-label">收藏列表</div>
<div class="card" id="bingFavList"></div>
</div>
</div>
<!-- 纯色/渐变 -->
<div class="section wp-type-section" id="sec-color">
<div class="section-label">纯色 / 渐变</div>
<div class="card">
<div class="item">
<div class="item-label">选择颜色</div>
<div class="btn-group">
<button class="btn" id="btnSolidColor">纯色</button>
<button class="btn" id="btnGradientColor">渐变</button>
</div>
</div>
<div class="item" id="currentColorRow" style="display:none">
<div class="item-label">当前颜色</div>
<div style="display:flex;align-items:center;gap:6px">
<span id="currentColorPreview" style="display:inline-block;width:24px;height:16px;border-radius:3px;border:1px solid var(--input-border)"></span>
<button class="btn btn-sm" id="btnSaveColor">收藏</button>
</div>
</div>
</div>
<div id="savedColorsSection" style="display:none;margin-top:6px">
<div class="card" id="savedColorsGrid" style="padding:8px 12px;display:flex;flex-wrap:wrap;gap:6px">
</div>
</div>
</div>
<!-- 布局 -->
<div class="section">
<div class="section-label">布局</div>
<div class="card">
<div class="item">
<div class="item-label">信息布局</div>
<select id="layout">
<option value="single">合并卡片</option>
<option value="multi">独立卡片</option>
</select>
</div>
</div>
</div>
<!-- 星座 + 城市 -->
<div class="section">
<div class="section-label">个性化</div>
<div class="card">
<div class="item">
<div class="item-label">我的星座</div>
<select id="zodiacSelect">
<option>白羊座</option><option>金牛座</option><option>双子座</option>
<option>巨蟹座</option><option>狮子座</option><option>处女座</option>
<option>天秤座</option><option>天蝎座</option><option>射手座</option>
<option>摩羯座</option><option>水瓶座</option><option>双鱼座</option>
</select>
</div>
<div class="item">
<div><div class="item-label">知识关键字</div><div class="item-desc">AI 将根据关键字生成知识小卡片</div></div>
<input type="text" id="knowledgeKeyword" placeholder="如: 历史、科学、冷知识">
</div>
<div class="item">
<div><div class="item-label">知识提示词</div><div class="item-desc">自定义生成风格,不会显示在桌面</div></div>
<input type="text" id="knowledgePrompt" placeholder="如: 用幽默口吻、面向程序员">
</div>
<div class="item" style="position:relative">
<div class="item-label">天气城市</div>
<div class="city-picker" id="cityPicker" tabindex="0">
<span class="city-picker-text" id="cityPickerText">未选择</span>
<span class="city-picker-arrow"></span>
<div class="city-panel" id="cityPanel">
<div class="city-col" id="provCol"></div>
<div class="city-col" id="cityCol"></div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">u-desktop v1.0</div>
<script>
var toggleKeys = ['wallpaper', 'time', 'showSeconds', 'weather', 'zodiacCard', 'knowledgeCard', 'ainewsCard'];
var initDone = false;
function sendToggle() {
if (!initDone) return;
var data = {};
toggleKeys.forEach(function(k) { data[k] = document.getElementById(k).checked; });
if (window.saveToggles) window.saveToggles(JSON.stringify(data));
}
toggleKeys.forEach(function(k) {
document.getElementById(k).addEventListener('change', sendToggle);
});
document.getElementById('layout').addEventListener('change', function() {
if (window.saveLayout) window.saveLayout(this.value);
});
document.getElementById('zodiacSelect').addEventListener('change', function() {
if (window.saveZodiac) window.saveZodiac(this.value);
});
var kwTimer = null;
document.getElementById('knowledgeKeyword').addEventListener('input', function() {
clearTimeout(kwTimer);
var val = this.value;
kwTimer = setTimeout(function() {
if (window.saveKnowledgeKeyword) window.saveKnowledgeKeyword(val);
}, 500);
});
var kpTimer = null;
document.getElementById('knowledgePrompt').addEventListener('input', function() {
clearTimeout(kpTimer);
var val = this.value;
kpTimer = setTimeout(function() {
if (window.saveKnowledgePrompt) window.saveKnowledgePrompt(val);
}, 500);
});
// City picker
var cityMap = {};
var selectedCityId = '';
var selectedCityName = '';
var picker = document.getElementById('cityPicker');
var pickerText = document.getElementById('cityPickerText');
var provCol = document.getElementById('provCol');
var cityCol = document.getElementById('cityCol');
var activeProv = '';
function renderProvinces(provinces) {
provCol.innerHTML = '';
provinces.forEach(function(p) {
var div = document.createElement('div');
div.textContent = p;
if (p === activeProv) div.className = 'active';
div.addEventListener('click', function(e) {
e.stopPropagation();
activeProv = p;
renderProvinces(provinces);
renderCities(p);
});
provCol.appendChild(div);
});
}
function renderCities(prov) {
cityCol.innerHTML = '';
var list = cityMap[prov] || [];
list.forEach(function(c) {
var div = document.createElement('div');
div.textContent = c.name;
if (c.id === selectedCityId) div.className = 'active';
div.addEventListener('click', function(e) {
e.stopPropagation();
selectedCityId = c.id;
selectedCityName = c.name;
pickerText.textContent = activeProv + ' · ' + c.name;
picker.classList.remove('open');
renderCities(prov);
if (window.saveCity) window.saveCity(c.id);
});
cityCol.appendChild(div);
});
}
picker.addEventListener('click', function(e) {
e.stopPropagation();
this.classList.toggle('open');
this.focus();
});
document.addEventListener('click', function(e) {
if (!picker.contains(e.target)) picker.classList.remove('open');
});
document.getElementById('themeSelect').addEventListener('change', function() {
if (window.saveWallpaperType) window.saveWallpaperType('theme', this.value);
document.getElementById('textInputRow').style.display = this.value === 'text' ? 'flex' : 'none';
});
var textTimer = null;
document.getElementById('wallpaperText').addEventListener('input', function() {
clearTimeout(textTimer);
var val = this.value;
textTimer = setTimeout(function() {
if (window.saveWallpaperText) window.saveWallpaperText(val);
}, 500);
});
var wpTypeTabs = document.querySelectorAll('#wpTypeTabs .btn');
var currentWpType = '';
function setWpType(type) {
wpTypeTabs.forEach(function(b) { b.classList.toggle('active', b.dataset.type === type); });
document.querySelectorAll('.wp-type-section').forEach(function(s) { s.classList.remove('visible'); });
var sec = document.getElementById('sec-' + type);
if (sec) sec.classList.add('visible');
if (type === 'bing' && currentWpType !== 'bing') {
if (window.enableBing) window.enableBing();
}
currentWpType = type;
}
wpTypeTabs.forEach(function(b) {
b.addEventListener('click', function() { setWpType(b.dataset.type); });
});
document.getElementById('btnPickImage').addEventListener('click', function() {
if (!window.pickLocalImage) return;
window.pickLocalImage().then(function(path) {
if (path) document.getElementById('imagePathDisplay').textContent = path;
});
});
function updateBingUI(stateJson) {
if (!stateJson) return;
var s = JSON.parse(stateJson);
document.getElementById('btnBingFav').textContent = s.label;
document.getElementById('bingCopyright').textContent = s.copyright || 'Bing 每日壁纸';
document.getElementById('bingIdx').textContent = (s.total > 0) ? ((s.idx + 1) + ' / ' + s.total) : '';
// load preview thumbnail
if (s.filename && window.bingThumbDataURI) {
var preview = document.getElementById('bingPreview');
window.bingThumbDataURI(s.filename).then(function(uri) {
if (uri) { preview.src = uri; preview.style.display = 'block'; }
});
}
}
function loadBingFavorites() {
if (!window.getBingFavorites) return;
window.getBingFavorites().then(function(json) {
var favs = JSON.parse(json);
var sec = document.getElementById('bingFavSection');
var list = document.getElementById('bingFavList');
list.innerHTML = '';
if (!favs || favs.length === 0) { sec.style.display = 'none'; return; }
sec.style.display = 'block';
list.style.cssText = 'padding:8px 12px;display:flex;flex-wrap:wrap;gap:6px';
favs.forEach(function(f) {
var img = document.createElement('img');
img.style.cssText = 'width:64px;height:40px;object-fit:cover;border-radius:4px;cursor:pointer;border:2px solid transparent;transition:border-color 0.15s';
img.title = f.copyright + ' (' + f.date + ')';
img.addEventListener('click', function() {
if (window.bingSetByIdx) window.bingSetByIdx(f.idx).then(function(s) { updateBingUI(s); });
});
img.addEventListener('mouseenter', function() { this.style.borderColor = 'var(--accent)'; });
img.addEventListener('mouseleave', function() { this.style.borderColor = 'transparent'; });
list.appendChild(img);
if (window.bingThumbDataURI) {
window.bingThumbDataURI(f.filename).then(function(uri) {
if (uri) img.src = uri;
});
}
});
});
}
document.getElementById('btnBingPrev').addEventListener('click', function() {
if (window.bingNext) window.bingNext().then(function(s) { updateBingUI(s); });
});
document.getElementById('btnBingNext').addEventListener('click', function() {
if (window.bingPrev) window.bingPrev().then(function(s) { updateBingUI(s); });
});
document.getElementById('btnBingFav').addEventListener('click', function() {
if (window.bingToggleFavorite) {
window.bingToggleFavorite().then(function(s) { updateBingUI(s); loadBingFavorites(); });
}
});
document.getElementById('bingAutoRefresh').addEventListener('change', function() {
if (window.saveBingAutoRefresh) window.saveBingAutoRefresh(this.checked);
});
document.getElementById('btnSolidColor').addEventListener('click', function() {
if (window.pickSolidColor) {
window.pickSolidColor().then(function(c) {
if (c) { currentColor1 = c; currentColor2 = ''; currentGradient = false; updateColorPreview(); }
});
}
});
document.getElementById('btnGradientColor').addEventListener('click', function() {
if (window.pickGradientColor) {
window.pickGradientColor().then(function(res) {
if (res) {
var parts = res.split(',');
currentColor1 = parts[0]; currentColor2 = parts[1] || ''; currentGradient = true;
updateColorPreview();
}
});
}
});
// Color favorites
var currentColor1 = '', currentColor2 = '', currentGradient = false;
var savedColors = [];
function updateColorPreview() {
var row = document.getElementById('currentColorRow');
var preview = document.getElementById('currentColorPreview');
if (!currentColor1) { row.style.display = 'none'; return; }
row.style.display = 'flex';
if (currentGradient && currentColor2) {
preview.style.background = 'linear-gradient(135deg,' + currentColor1 + ',' + currentColor2 + ')';
} else {
preview.style.background = currentColor1;
}
}
function renderSavedColors() {
var sec = document.getElementById('savedColorsSection');
var grid = document.getElementById('savedColorsGrid');
grid.innerHTML = '';
if (!savedColors || savedColors.length === 0) { sec.style.display = 'none'; return; }
sec.style.display = 'block';
savedColors.forEach(function(sc, idx) {
var swatch = document.createElement('div');
swatch.className = 'color-swatch';
if (sc.gradient && sc.color2) {
swatch.style.background = 'linear-gradient(135deg,' + sc.color1 + ',' + sc.color2 + ')';
} else {
swatch.style.background = sc.color1;
}
var del = document.createElement('span');
del.className = 'del'; del.textContent = 'x';
del.addEventListener('click', function(e) {
e.stopPropagation();
if (window.removeSavedColor) window.removeSavedColor(idx).then(function() {
savedColors.splice(idx, 1);
renderSavedColors();
});
});
swatch.appendChild(del);
swatch.addEventListener('click', function() {
if (window.applySavedColor) window.applySavedColor(idx);
currentColor1 = sc.color1; currentColor2 = sc.color2; currentGradient = sc.gradient;
updateColorPreview();
});
grid.appendChild(swatch);
});
}
document.getElementById('btnSaveColor').addEventListener('click', function() {
if (!currentColor1) return;
if (window.addSavedColor) window.addSavedColor(currentColor1, currentColor2, currentGradient).then(function() {
savedColors.push({color1: currentColor1, color2: currentColor2, gradient: currentGradient});
renderSavedColors();
});
});
if (window.loadAllSettings) {
window.loadAllSettings().then(function(raw) {
var s = JSON.parse(raw);
// Apply system theme
if (s.lightTheme) {
document.documentElement.className = 'light';
}
// Province-city cascade
cityMap = s.citiesByProv || {};
selectedCityId = s.city || '';
activeProv = '';
if (s.provinces && s.provinces.length) {
for (var p in cityMap) {
for (var ci = 0; ci < cityMap[p].length; ci++) {
if (cityMap[p][ci].id === selectedCityId) { activeProv = p; selectedCityName = cityMap[p][ci].name; break; }
}
if (activeProv) break;
}
renderProvinces(s.provinces);
if (activeProv) renderCities(activeProv);
if (activeProv && selectedCityName) {
pickerText.textContent = activeProv + ' · ' + selectedCityName;
}
}
if (s.themes && s.themes.length) {
var themeSel = document.getElementById('themeSelect');
s.themes.forEach(function(t) {
var opt = document.createElement('option');
opt.value = t.value;
opt.textContent = t.label;
themeSel.appendChild(opt);
});
}
toggleKeys.forEach(function(k) {
if (s[k] !== undefined) document.getElementById(k).checked = s[k];
});
if (s.layout) document.getElementById('layout').value = s.layout;
if (s.zodiac) document.getElementById('zodiacSelect').value = s.zodiac;
if (s.theme) document.getElementById('themeSelect').value = s.theme;
if (s.wallpaperText) document.getElementById('wallpaperText').value = s.wallpaperText;
if (s.imagePath) document.getElementById('imagePathDisplay').textContent = s.imagePath;
if (s.knowledgeKeyword) document.getElementById('knowledgeKeyword').value = s.knowledgeKeyword;
if (s.knowledgePrompt) document.getElementById('knowledgePrompt').value = s.knowledgePrompt;
if (s.theme === 'text') document.getElementById('textInputRow').style.display = 'flex';
// Color state
if (s.color1) { currentColor1 = s.color1; currentColor2 = s.color2 || ''; currentGradient = s.colorGradient || false; }
if (s.wallpaperType === 'color') updateColorPreview();
savedColors = s.savedColors || [];
renderSavedColors();
setWpType(s.wallpaperType || 'theme');
// Bing state
if (s.wallpaperType === 'bing' && window.getBingInfo) {
window.getBingInfo().then(function(si) { updateBingUI(si); });
}
if (s.wallpaperType === 'bing') loadBingFavorites();
if (s.bingAutoRefresh !== undefined) document.getElementById('bingAutoRefresh').checked = s.bingAutoRefresh;
// resize window to fit content
setTimeout(function() {
var el = document.documentElement;
var cw = el.scrollWidth;
var ch = el.scrollHeight + 8;
if (window.resizeToFit) window.resizeToFit(cw, ch);
}, 100);
initDone = true;
});
}
</script>
</body>
</html>

239
web/themes/fractal_src.html Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Dynamic Wallpaper</title>
<style>
* { margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
#info {
position: fixed; top: 20px; right: 20px;
color: rgba(255,255,255,0.6); font: 14px monospace;
pointer-events: none; user-select: none;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="info"></div>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) { document.body.innerHTML = '<h1 style="color:#fff;text-align:center;margin-top:40vh">WebGL 不可用</h1>'; }
let paused = false;
let fullscreen = false;
let mouseX = 0, mouseY = 0;
let lastMove = 0;
// --- 状态控制 (Go Bridge) ---
window.setPaused = function(v) { paused = v; };
window.setFullscreen = function(v) { fullscreen = v; };
// --- 鼠标 ---
document.addEventListener('mousemove', e => {
mouseX = e.clientX / window.innerWidth;
mouseY = 1.0 - e.clientY / window.innerHeight;
lastMove = performance.now();
});
document.addEventListener('click', e => {
// 点击涟漪效果 — 传给 shader
clickX = e.clientX / window.innerWidth;
clickY = 1.0 - e.clientY / window.innerHeight;
clickTime = performance.now();
});
let clickX = 0, clickY = 0, clickTime = 0;
// --- Shader ---
const vertSrc = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
// 极光 + 流体噪声 shader
const fragSrc = `
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_click;
uniform vec2 u_clickPos;
// simplex-like noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m; m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 p = uv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
float t = u_time * 0.3;
// 鼠标影响
vec2 mp = u_mouse * 2.0 - 1.0;
mp.x *= u_resolution.x / u_resolution.y;
float mouseDist = length(p - mp);
float mouseInfluence = smoothstep(0.8, 0.0, mouseDist) * 0.3;
// 点击涟漪
float ripple = 0.0;
if (u_click > 0.0) {
vec2 cp = u_clickPos * 2.0 - 1.0;
cp.x *= u_resolution.x / u_resolution.y;
float d = length(p - cp);
ripple = sin(d * 30.0 - u_click * 8.0) * exp(-d * 3.0) * exp(-u_click * 2.0) * 0.15;
}
// 多层噪声
float n1 = snoise(p * 1.5 + vec2(t * 0.5, t * 0.3)) * 0.5 + 0.5;
float n2 = snoise(p * 3.0 + vec2(-t * 0.7, t * 0.5)) * 0.5 + 0.5;
float n3 = snoise(p * 0.8 + vec2(t * 0.2, -t * 0.4) + mouseInfluence) * 0.5 + 0.5;
// 极光色彩
vec3 c1 = vec3(0.05, 0.2, 0.4); // 深蓝
vec3 c2 = vec3(0.0, 0.8, 0.5); // 翠绿
vec3 c3 = vec3(0.3, 0.1, 0.6); // 紫色
vec3 c4 = vec3(0.1, 0.5, 0.9); // 天蓝
// 极光带
float aurora = smoothstep(0.3, 0.7, n3) * smoothstep(0.8, 0.4, n1);
vec3 auroraColor = mix(c2, c4, n2) * aurora * 1.2;
// 底层渐变
vec3 bg = mix(c1, c3, uv.y * 0.5 + n1 * 0.3);
bg += auroraColor;
// 星星效果
float stars = pow(snoise(p * 20.0), 12.0) * 0.8;
bg += vec3(stars);
// 涟漪叠加
bg += vec3(ripple * 2.0, ripple * 3.0, ripple * 4.0);
// 鼠标光晕
bg += vec3(0.1, 0.3, 0.5) * mouseInfluence;
// 轻微暗角
float vignette = 1.0 - smoothstep(0.5, 1.5, length(p * 0.7));
bg *= vignette * 0.9 + 0.1;
gl_FragColor = vec4(bg, 1.0);
}
`;
// --- 编译 Shader ---
function createShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error('Shader error:', gl.getShaderInfoLog(s));
return null;
}
return s;
}
const vs = createShader(gl.VERTEX_SHADER, vertSrc);
const fs = createShader(gl.FRAGMENT_SHADER, fragSrc);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
gl.useProgram(prog);
// 全屏四边形
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// Uniforms
const uTime = gl.getUniformLocation(prog, 'u_time');
const uRes = gl.getUniformLocation(prog, 'u_resolution');
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
const uClick = gl.getUniformLocation(prog, 'u_click');
const uClickPos = gl.getUniformLocation(prog, 'u_clickPos');
// --- 渲染循环 ---
let lastFrame = 0;
let targetFPS = 30;
let currentFPS = 30;
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
function render(now) {
requestAnimationFrame(render);
if (paused || fullscreen) return;
// 帧率控制
const interval = 1000 / targetFPS;
if (now - lastFrame < interval) return;
lastFrame = now;
// 自适应帧率:鼠标静止 5s 后降帧
const idleTime = now - lastMove;
if (idleTime > 5000) {
targetFPS = 10;
} else {
targetFPS = 30;
}
const t = now / 1000.0;
const clickElapsed = clickTime > 0 ? (now - clickTime) / 1000.0 : 0.0;
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform2f(uMouse, mouseX, mouseY);
gl.uniform1f(uClick, clickElapsed > 3.0 ? 0.0 : clickElapsed);
gl.uniform2f(uClickPos, clickX, clickY);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
requestAnimationFrame(render);
// 通知 Go 层壁纸就绪
if (window.wallpaperReady) {
wallpaperReady().then(() => console.log('wallpaper embedded'));
}
</script>
</body>
</html>

View File

@@ -21,6 +21,9 @@ var (
procShowWindow = user32.NewProc("ShowWindow")
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW")
procGetDpiForWindow = user32.NewProc("GetDpiForWindow")
procGetClientRect = user32.NewProc("GetClientRect")
procGetMessageW = user32.NewProc("GetMessageW")
procPostMessageW = user32.NewProc("PostMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
@@ -69,6 +72,14 @@ func findWorkerW() uintptr {
return ww
}
func getDPI(hwnd uintptr) int {
dpi, _, _ := procGetDpiForWindow.Call(hwnd)
if dpi == 0 {
return 96
}
return int(dpi)
}
func getScreenSize() (int32, int32) {
w, _, _ := procGetSystemMetrics.Call(0)
h, _, _ := procGetSystemMetrics.Call(1)