diff --git a/ainews.go b/ainews.go new file mode 100644 index 0000000..c70f8cc --- /dev/null +++ b/ainews.go @@ -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) + } + }() +} diff --git a/bing.go b/bing.go index ae09dba..0cb32d5 100644 --- a/bing.go +++ b/bing.go @@ -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,80 +68,82 @@ 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 } - for _, img := range br.Images { - date := img.StartDate - if date == "" { - continue - } - if _, exists := existingMap[date]; exists { - continue - } - - imgURL := img.URL - if !strings.HasPrefix(imgURL, "http") { - imgURL = "https://www.bing.com" + imgURL - } - - imgResp, err := httpClient.Get(imgURL) + // 分页下载: 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 图片下载失败 (%s): %v", date, err) - continue + log.Printf("Bing API 请求失败 (idx=%d): %v", idx, err) + break } - imgData, _ := io.ReadAll(imgResp.Body) - imgResp.Body.Close() - if len(imgData) == 0 { - continue + data, _ := io.ReadAll(resp.Body) + resp.Body.Close() + var br bingResponse + if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 { + break } - 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 - } - log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData)) + newCount := 0 + for _, img := range br.Images { + date := img.StartDate + if date == "" { + continue + } + if _, exists := existingMap[date]; exists { + continue + } - existingMap[date] = BingRecord{ - Date: date, - URLBase: img.URLBase, - Copyright: img.Copyright, - Filename: filename, + imgURL := img.URL + if !strings.HasPrefix(imgURL, "http") { + imgURL = "https://www.bing.com" + imgURL + } + + imgResp, err := httpClient.Get(imgURL) + if err != nil { + continue + } + imgData, _ := io.ReadAll(imgResp.Body) + imgResp.Body.Close() + if len(imgData) == 0 { + continue + } + + filename := date + ".jpg" + localPath := filepath.Join(bingDir(), filename) + os.WriteFile(localPath, imgData, 0644) + log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData)) + + existingMap[date] = BingRecord{ + Date: date, + URLBase: img.URLBase, + 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 { - records = append(records, r) - } - } - if len(records) == 0 { - return + for _, r := range existingMap { + records = append(records, r) } + 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++ - saveBingHistory(h) - log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date) - reloadWallpaper() - } + h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records) + saveBingHistory(h) + log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date) + bingReloadImage() } func bingNext() { @@ -191,12 +193,13 @@ func bingNext() { defer bingMu.Unlock() h := loadBingHistory() - if h.CurrentIdx > 0 { - h.CurrentIdx-- - saveBingHistory(h) - log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date) - reloadWallpaper() + 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) + 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() } } } diff --git a/config.go b/config.go index 6d537d1..def1793 100644 --- a/config.go +++ b/config.go @@ -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 = "射手座" diff --git a/go.mod b/go.mod index 3753988..5cb0c1d 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index beeb2a3..341bdc0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/horoscope.go b/horoscope.go new file mode 100644 index 0000000..5f9702f --- /dev/null +++ b/horoscope.go @@ -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) + }() +} diff --git a/knowledge.go b/knowledge.go new file mode 100644 index 0000000..be4a528 --- /dev/null +++ b/knowledge.go @@ -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() +} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..43544bb --- /dev/null +++ b/settings.go @@ -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("设置窗口已关闭") + }() +} diff --git a/systray.go b/systray.go index b74f3b2..90bc9cb 100644 --- a/systray.go +++ b/systray.go @@ -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() { diff --git a/wallpaper.go b/wallpaper.go index 6842b36..8c79662 100644 --- a/wallpaper.go +++ b/wallpaper.go @@ -78,9 +78,39 @@ func buildWallpaperHTML(cfg *Config) string { if bg == "" { bg = themeAurora } - html := strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1) + bgWrapped := fmt.Sprintf(`