新增: 星座运势+AI资讯+知识卡片+桌面设置窗口+秒显示开关
- 星座运势: 天聚数行API集成,5维进度条+幸运标签+今日概述 - AI资讯: 天聚数行API,图文布局5条展示,文件缓存2小时刷新 - 知识卡片: AI生成,关键字+提示词配置,30分钟刷新 - 桌面设置: 独立WebView2窗口,760x1350,含壁纸/布局/城市/颜色等配置 - 显示控制: 壁纸/时间/天气/星座/知识/AI资讯独立开关,秒显示开关 - 文件缓存: 星座运势+AI资讯缓存到本地,启动即显示上次数据 - initDone防抖: 防止设置窗口初始化触发卡片重载
This commit is contained in:
165
ainews.go
Normal file
165
ainews.go
Normal 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
162
bing.go
@@ -7,12 +7,13 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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 {
|
type bingResponse struct {
|
||||||
Images []struct {
|
Images []struct {
|
||||||
@@ -67,30 +68,29 @@ func fetchBingHistory() {
|
|||||||
bingMu.Lock()
|
bingMu.Lock()
|
||||||
defer bingMu.Unlock()
|
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)
|
os.MkdirAll(bingDir(), 0755)
|
||||||
|
|
||||||
existing := loadBingHistory()
|
existing := loadBingHistory()
|
||||||
existingMap := make(map[string]BingRecord)
|
existingMap := make(map[string]BingRecord)
|
||||||
for _, r := range existing.Records {
|
for _, r := range existing.Records {
|
||||||
existingMap[r.Date] = r
|
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 {
|
for _, img := range br.Images {
|
||||||
date := img.StartDate
|
date := img.StartDate
|
||||||
if date == "" {
|
if date == "" {
|
||||||
@@ -107,7 +107,6 @@ func fetchBingHistory() {
|
|||||||
|
|
||||||
imgResp, err := httpClient.Get(imgURL)
|
imgResp, err := httpClient.Get(imgURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Bing 图片下载失败 (%s): %v", date, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
imgData, _ := io.ReadAll(imgResp.Body)
|
imgData, _ := io.ReadAll(imgResp.Body)
|
||||||
@@ -118,10 +117,7 @@ func fetchBingHistory() {
|
|||||||
|
|
||||||
filename := date + ".jpg"
|
filename := date + ".jpg"
|
||||||
localPath := filepath.Join(bingDir(), filename)
|
localPath := filepath.Join(bingDir(), filename)
|
||||||
if err := os.WriteFile(localPath, imgData, 0644); err != nil {
|
os.WriteFile(localPath, imgData, 0644)
|
||||||
log.Printf("Bing 图片保存失败 (%s): %v", date, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
|
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
|
||||||
|
|
||||||
existingMap[date] = BingRecord{
|
existingMap[date] = BingRecord{
|
||||||
@@ -130,17 +126,24 @@ func fetchBingHistory() {
|
|||||||
Copyright: img.Copyright,
|
Copyright: img.Copyright,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
}
|
}
|
||||||
|
newCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newCount == 0 && idx > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 API 返回顺序重建 records (newest first)
|
||||||
|
// 用 existingMap 里所有记录按 date 降序排列
|
||||||
var records []BingRecord
|
var records []BingRecord
|
||||||
for _, img := range br.Images {
|
for _, r := range existingMap {
|
||||||
if r, ok := existingMap[img.StartDate]; ok {
|
|
||||||
records = append(records, r)
|
records = append(records, r)
|
||||||
}
|
}
|
||||||
}
|
sort.Slice(records, func(i, j int) bool {
|
||||||
if len(records) == 0 {
|
return records[i].Date > records[j].Date
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
history := &BingHistory{
|
history := &BingHistory{
|
||||||
Records: records,
|
Records: records,
|
||||||
@@ -151,6 +154,7 @@ func fetchBingHistory() {
|
|||||||
}
|
}
|
||||||
saveBingHistory(history)
|
saveBingHistory(history)
|
||||||
|
|
||||||
|
log.Printf("Bing 壁纸: 共 %d 张", len(records))
|
||||||
reloadWallpaper()
|
reloadWallpaper()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +182,10 @@ func bingPrev() {
|
|||||||
if len(h.Records) == 0 {
|
if len(h.Records) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.CurrentIdx < len(h.Records)-1 {
|
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
|
||||||
h.CurrentIdx++
|
|
||||||
saveBingHistory(h)
|
saveBingHistory(h)
|
||||||
log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
|
log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
|
||||||
reloadWallpaper()
|
bingReloadImage()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func bingNext() {
|
func bingNext() {
|
||||||
@@ -191,12 +193,13 @@ func bingNext() {
|
|||||||
defer bingMu.Unlock()
|
defer bingMu.Unlock()
|
||||||
|
|
||||||
h := loadBingHistory()
|
h := loadBingHistory()
|
||||||
if h.CurrentIdx > 0 {
|
if len(h.Records) == 0 {
|
||||||
h.CurrentIdx--
|
return
|
||||||
|
}
|
||||||
|
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
|
||||||
saveBingHistory(h)
|
saveBingHistory(h)
|
||||||
log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
|
log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date)
|
||||||
reloadWallpaper()
|
bingReloadImage()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func bingToggleFavorite() string {
|
func bingToggleFavorite() string {
|
||||||
@@ -229,6 +232,67 @@ func bingCopyrightInfo() string {
|
|||||||
return ""
|
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() {
|
func bingWallpaperLoop() {
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
if cfg.WallpaperType == WPBing {
|
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 {
|
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()
|
cfg := loadConfig()
|
||||||
if cfg.WallpaperType == WPBing {
|
if cfg.WallpaperType == WPBing {
|
||||||
fetchBingHistory()
|
go fetchBingHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
config.go
17
config.go
@@ -38,6 +38,12 @@ const (
|
|||||||
ThemeText ThemeName = "text"
|
ThemeText ThemeName = "text"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SavedColor struct {
|
||||||
|
Color1 string `json:"color1"`
|
||||||
|
Color2 string `json:"color2"`
|
||||||
|
Gradient bool `json:"gradient"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Zodiac string `json:"zodiac"`
|
Zodiac string `json:"zodiac"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
@@ -49,6 +55,17 @@ type Config struct {
|
|||||||
ColorGradient bool `json:"colorGradient"`
|
ColorGradient bool `json:"colorGradient"`
|
||||||
WallpaperText string `json:"wallpaperText"`
|
WallpaperText string `json:"wallpaperText"`
|
||||||
Layout Layout `json:"layout"`
|
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 = "射手座"
|
const defaultZodiac = "射手座"
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -3,19 +3,28 @@ module u-desktop
|
|||||||
go 1.26.3
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/getlantern/systray v1.2.2
|
||||||
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
|
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.45.0
|
||||||
|
modernc.org/sqlite v1.50.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // 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/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/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/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
52
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/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 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
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=
|
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/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 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
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 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo=
|
||||||
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo=
|
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 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
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/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/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 h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
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/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/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/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=
|
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-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-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/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.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 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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=
|
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
177
horoscope.go
Normal 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
220
knowledge.go
Normal 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
405
settings.go
Normal 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("设置窗口已关闭")
|
||||||
|
}()
|
||||||
|
}
|
||||||
288
systray.go
288
systray.go
@@ -15,296 +15,19 @@ import (
|
|||||||
"github.com/jchv/go-webview2"
|
"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() {
|
func onSystrayReady() {
|
||||||
systray.SetIcon(generateIcon())
|
systray.SetIcon(generateIcon())
|
||||||
systray.SetTooltip("动态壁纸引擎")
|
systray.SetTooltip("动态壁纸引擎")
|
||||||
|
|
||||||
cfg := loadConfig()
|
mSettings := systray.AddMenuItem("桌面设置", "打开设置窗口")
|
||||||
|
|
||||||
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()
|
|
||||||
mRestart := systray.AddMenuItem("重启", "重启程序")
|
mRestart := systray.AddMenuItem("重启", "重启程序")
|
||||||
mQuit := systray.AddMenuItem("退出", "退出程序")
|
mQuit := systray.AddMenuItem("退出", "退出程序")
|
||||||
|
|
||||||
// 布局切换
|
// 设置窗口
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
<-mLayoutSingle.ClickedCh
|
<-mSettings.ClickedCh
|
||||||
cfg := loadConfig()
|
openSettingsWindow()
|
||||||
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) + ")")
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -326,7 +49,10 @@ func onSystrayReady() {
|
|||||||
|
|
||||||
go startWebView()
|
go startWebView()
|
||||||
go weatherLoop()
|
go weatherLoop()
|
||||||
|
go horoscopeLoop()
|
||||||
|
go aiNewsLoop()
|
||||||
go bingWallpaperLoop()
|
go bingWallpaperLoop()
|
||||||
|
go knowledgeLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWebView() {
|
func startWebView() {
|
||||||
|
|||||||
47
wallpaper.go
47
wallpaper.go
@@ -78,9 +78,39 @@ func buildWallpaperHTML(cfg *Config) string {
|
|||||||
if bg == "" {
|
if bg == "" {
|
||||||
bg = themeAurora
|
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)
|
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 != "" {
|
if cfg.WallpaperType == WPTheme && cfg.Theme == ThemeText && cfg.WallpaperText != "" {
|
||||||
escaped := strings.ReplaceAll(cfg.WallpaperText, `\`, `\\`)
|
escaped := strings.ReplaceAll(cfg.WallpaperText, `\`, `\\`)
|
||||||
@@ -133,3 +163,18 @@ func reloadWallpaper() {
|
|||||||
go fetchAndPushWeather(city)
|
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))
|
||||||
|
}
|
||||||
|
|||||||
33
weather.go
33
weather.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +35,12 @@ var cities = []City{
|
|||||||
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
|
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
|
||||||
{"101250101", "长沙", "长沙", "湖南"},
|
{"101250101", "长沙", "长沙", "湖南"},
|
||||||
{"101270101", "成都", "成都", "四川"},
|
{"101270101", "成都", "成都", "四川"},
|
||||||
|
{"101090101", "石家庄", "石家庄", "河北"},
|
||||||
|
{"101090206", "任丘", "任丘", "河北"},
|
||||||
|
{"101090301", "邯郸", "邯郸", "河北"},
|
||||||
|
{"101290106", "宣威", "宣威", "云南"},
|
||||||
|
{"101290101", "昆明", "昆明", "云南"},
|
||||||
|
{"101260101", "贵阳", "贵阳", "贵州"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
|
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
|
||||||
@@ -174,7 +181,29 @@ type currentWeather struct {
|
|||||||
Temp string `json:"temp"`
|
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) {
|
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 {
|
type weatherData struct {
|
||||||
Current string `json:"current"`
|
Current string `json:"current"`
|
||||||
Hourly []hourlyItem `json:"hourly"`
|
Hourly []hourlyItem `json:"hourly"`
|
||||||
@@ -200,6 +229,10 @@ func fetchAndPushWeather(city City) {
|
|||||||
wd.Daily = fetchDailyForecast(city.ID)
|
wd.Daily = fetchDailyForecast(city.ID)
|
||||||
|
|
||||||
jsonData, _ := json.Marshal(wd)
|
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)))
|
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
|
||||||
log.Println("天气已推送:", wd.Current)
|
log.Println("天气已推送:", wd.Current)
|
||||||
}
|
}
|
||||||
|
|||||||
391
web/overlay.html
391
web/overlay.html
@@ -22,7 +22,6 @@ html, body {
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 24px 28px;
|
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);
|
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 {
|
.time {
|
||||||
@@ -34,6 +33,20 @@ html, body {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
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 {
|
.date {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(255,255,255,0.7);
|
color: rgba(255,255,255,0.7);
|
||||||
@@ -101,6 +114,152 @@ html, body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
|
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 {
|
.divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -131,7 +290,8 @@ body.layout-single #info {
|
|||||||
top: 40px;
|
top: 40px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
min-width: 320px;
|
width: calc(50vw - 60px);
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
body.layout-single #info .date {
|
body.layout-single #info .date {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -147,20 +307,65 @@ body.layout-multi #card-time {
|
|||||||
right: 40px;
|
right: 40px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
body.layout-multi #card-weather {
|
body.layout-multi #card-bottom {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
text-align: right;
|
width: calc(50vw - 60px);
|
||||||
min-width: 420px;
|
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;
|
position: fixed;
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
left: 40px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="layout-{{LAYOUT}}">
|
<body class="layout-{{LAYOUT}}">
|
||||||
@@ -172,6 +377,16 @@ body.layout-multi #card-zodiac {
|
|||||||
<div class="date" id="date">1月1日 周一</div>
|
<div class="date" id="date">1月1日 周一</div>
|
||||||
<div class="time" id="time">00:00</div>
|
<div class="time" id="time">00:00</div>
|
||||||
<div class="divider"></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>
|
||||||
<div class="current-weather" id="currentWeather">加载中...</div>
|
<div class="current-weather" id="currentWeather">加载中...</div>
|
||||||
<div class="forecast-title">24小时预报</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="date" id="date2">1月1日 周一</div>
|
||||||
<div class="time" id="time2">00:00</div>
|
<div class="time" id="time2">00:00</div>
|
||||||
</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 id="card-weather" class="card">
|
||||||
<div class="current-weather" id="currentWeather2">加载中...</div>
|
<div class="current-weather" id="currentWeather2">加载中...</div>
|
||||||
<div class="forecast-title">24小时预报</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="forecast-title" style="margin-top:12px">7日预报</div>
|
||||||
<div class="daily-forecast" id="dailyForecast2"></div>
|
<div class="daily-forecast" id="dailyForecast2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="card-zodiac" class="card">
|
</div>
|
||||||
<div class="zodiac-text" id="zodiac2">加载中...</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -206,22 +433,25 @@ body.layout-multi #card-zodiac {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
var lastTimeStr='', lastDateStr='', lastZodiac='';
|
var lastTimeStr='', lastDateStr='', lastZodiac='';
|
||||||
|
var horoscopeInfo=null;
|
||||||
|
|
||||||
var zodiacData = {
|
var zodiacData = {
|
||||||
'白羊座':{icon:'♈',date:'3.21-4.19',fortune:'今日运势旺盛,适合开展新计划。'},
|
'白羊座':{icon:'♈',date:'3.21-4.19'},
|
||||||
'金牛座':{icon:'♉',date:'4.20-5.20',fortune:'财运不错,但需注意健康。'},
|
'金牛座':{icon:'♉',date:'4.20-5.20'},
|
||||||
'双子座':{icon:'♊',date:'5.21-6.21',fortune:'人际关系活跃,社交运势佳。'},
|
'双子座':{icon:'♊',date:'5.21-6.21'},
|
||||||
'巨蟹座':{icon:'♋',date:'6.22-7.22',fortune:'情绪敏感,适合独处思考。'},
|
'巨蟹座':{icon:'♋',date:'6.22-7.22'},
|
||||||
'狮子座':{icon:'♌',date:'7.23-8.22',fortune:'自信爆棚,工作表现突出。'},
|
'狮子座':{icon:'♌',date:'7.23-8.22'},
|
||||||
'处女座':{icon:'♍',date:'8.23-9.22',fortune:'细节决定成败,专注当下。'},
|
'处女座':{icon:'♍',date:'8.23-9.22'},
|
||||||
'天秤座':{icon:'♎',date:'9.23-10.23',fortune:'感情运佳,单身者有机会。'},
|
'天秤座':{icon:'♎',date:'9.23-10.23'},
|
||||||
'天蝎座':{icon:'♏',date:'10.24-11.22',fortune:'直觉敏锐,适合做决策。'},
|
'天蝎座':{icon:'♏',date:'10.24-11.22'},
|
||||||
'射手座':{icon:'♐',date:'11.23-12.21',fortune:'冒险精神旺盛,出行注意安全。'},
|
'射手座':{icon:'♐',date:'11.23-12.21'},
|
||||||
'摩羯座':{icon:'♑',date:'12.22-1.19',fortune:'事业运佳,工作效率高。'},
|
'摩羯座':{icon:'♑',date:'12.22-1.19'},
|
||||||
'水瓶座':{icon:'♒',date:'1.20-2.18',fortune:'创新思维活跃,灵感不断。'},
|
'水瓶座':{icon:'♒',date:'1.20-2.18'},
|
||||||
'双鱼座':{icon:'♓',date:'2.19-3.20',fortune:'艺术灵感丰富,适合创作。'}
|
'双鱼座':{icon:'♓',date:'2.19-3.20'}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var barColors={all:'#e0e0e0',love:'#ff6b9d',work:'#4fc3f7',money:'#ffd54f',health:'#81c784'};
|
||||||
|
|
||||||
function getUserZodiac(){ return window.userZodiac||'射手座'; }
|
function getUserZodiac(){ return window.userZodiac||'射手座'; }
|
||||||
|
|
||||||
function setEl(id,html){
|
function setEl(id,html){
|
||||||
@@ -233,14 +463,52 @@ function setText(id,txt){
|
|||||||
if(e) e.textContent=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(){
|
function updateZodiacDisplay(){
|
||||||
var name=getUserZodiac();
|
var name=getUserZodiac();
|
||||||
if(name===lastZodiac) return;
|
if(name===lastZodiac) return;
|
||||||
lastZodiac=name;
|
lastZodiac=name;
|
||||||
var z=zodiacData[name]||{icon:'✨',date:'',fortune:'运势平稳,保持平常心。'};
|
var html=buildZodiacHTML(name);
|
||||||
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>';
|
|
||||||
setEl('zodiac',html);
|
setEl('zodiac',html);
|
||||||
setEl('zodiac2',html);
|
setEl('zodiac2',html);
|
||||||
|
syncZodiacHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
var holidays=[
|
var holidays=[
|
||||||
@@ -266,12 +534,29 @@ function getNextHoliday(now){
|
|||||||
var target=new Date(y,h.m-1,h.d);
|
var target=new Date(y,h.m-1,h.d);
|
||||||
var diff=Math.ceil((target-now)/(1000*60*60*24));
|
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&&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;});
|
results.sort(function(a,b){return a.diff-b.diff;});
|
||||||
return results.length>0?'距'+results[0].name+'还有'+results[0].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(){
|
function updateTime(){
|
||||||
var now=new Date();
|
var now=new Date();
|
||||||
var hh=String(now.getHours()).padStart(2,'0');
|
var hh=String(now.getHours()).padStart(2,'0');
|
||||||
@@ -280,10 +565,21 @@ function updateTime(){
|
|||||||
var month=now.getMonth()+1;
|
var month=now.getMonth()+1;
|
||||||
var day=now.getDate();
|
var day=now.getDate();
|
||||||
var week=['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
|
var week=['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
|
||||||
var timeStr=hh+':'+mm+':'+ss;
|
var timeStr=window._showSeconds?(hh+':'+mm+':'+ss):(hh+':'+mm);
|
||||||
var dateStr=month+'月'+day+'日 '+week;
|
var dateStr=month+'月'+day+'日 '+week;
|
||||||
if(timeStr!==lastTimeStr){
|
if(timeStr!==lastTimeStr){
|
||||||
setText('time',timeStr); setText('time2',timeStr); lastTimeStr=timeStr;
|
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){
|
if(dateStr!==lastDateStr){
|
||||||
var holiday=getNextHoliday(now);
|
var holiday=getNextHoliday(now);
|
||||||
@@ -293,6 +589,50 @@ function updateTime(){
|
|||||||
updateZodiacDisplay();
|
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){
|
window.updateWeatherFromGo=function(data){
|
||||||
if(typeof data==='string') data=JSON.parse(data);
|
if(typeof data==='string') data=JSON.parse(data);
|
||||||
|
|
||||||
@@ -318,6 +658,7 @@ window.updateWeatherFromGo=function(data){
|
|||||||
|
|
||||||
renderWeather('currentWeather','hourlyForecast','dailyForecast');
|
renderWeather('currentWeather','hourlyForecast','dailyForecast');
|
||||||
renderWeather('currentWeather2','hourlyForecast2','dailyForecast2');
|
renderWeather('currentWeather2','hourlyForecast2','dailyForecast2');
|
||||||
|
syncZodiacHeight();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateTime();
|
updateTime();
|
||||||
|
|||||||
699
web/settings.html
Normal file
699
web/settings.html
Normal 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
239
web/themes/fractal_src.html
Normal 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>
|
||||||
11
win32.go
11
win32.go
@@ -21,6 +21,9 @@ var (
|
|||||||
procShowWindow = user32.NewProc("ShowWindow")
|
procShowWindow = user32.NewProc("ShowWindow")
|
||||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
||||||
|
procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW")
|
||||||
|
procGetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
||||||
|
procGetClientRect = user32.NewProc("GetClientRect")
|
||||||
procGetMessageW = user32.NewProc("GetMessageW")
|
procGetMessageW = user32.NewProc("GetMessageW")
|
||||||
procPostMessageW = user32.NewProc("PostMessageW")
|
procPostMessageW = user32.NewProc("PostMessageW")
|
||||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||||
@@ -69,6 +72,14 @@ func findWorkerW() uintptr {
|
|||||||
return ww
|
return ww
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDPI(hwnd uintptr) int {
|
||||||
|
dpi, _, _ := procGetDpiForWindow.Call(hwnd)
|
||||||
|
if dpi == 0 {
|
||||||
|
return 96
|
||||||
|
}
|
||||||
|
return int(dpi)
|
||||||
|
}
|
||||||
|
|
||||||
func getScreenSize() (int32, int32) {
|
func getScreenSize() (int32, int32) {
|
||||||
w, _, _ := procGetSystemMetrics.Call(0)
|
w, _, _ := procGetSystemMetrics.Call(0)
|
||||||
h, _, _ := procGetSystemMetrics.Call(1)
|
h, _, _ := procGetSystemMetrics.Call(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user