diff --git a/bing.go b/bing.go index b1ba3a4..ae09dba 100644 --- a/bing.go +++ b/bing.go @@ -2,25 +2,71 @@ package main import ( "encoding/json" + "fmt" "io" "log" "os" "path/filepath" "strings" + "sync" "time" ) -const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN" +const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=8&mkt=zh-CN" type bingResponse struct { Images []struct { + StartDate string `json:"startdate"` URL string `json:"url"` URLBase string `json:"urlbase"` Copyright string `json:"copyright"` } `json:"images"` } -func fetchBingWallpaper() { +type BingRecord struct { + Date string `json:"date"` + URLBase string `json:"urlbase"` + Copyright string `json:"copyright"` + Filename string `json:"filename"` + Favorited bool `json:"favorited"` +} + +type BingHistory struct { + Records []BingRecord `json:"records"` + CurrentIdx int `json:"currentIdx"` +} + +var bingMu sync.Mutex + +func bingDir() string { + return filepath.Join(configDir(), "bing") +} + +func bingHistoryPath() string { + return filepath.Join(configDir(), "bing_history.json") +} + +func loadBingHistory() *BingHistory { + data, err := os.ReadFile(bingHistoryPath()) + if err != nil { + return &BingHistory{} + } + var h BingHistory + if json.Unmarshal(data, &h) != nil { + return &BingHistory{} + } + return &h +} + +func saveBingHistory(h *BingHistory) error { + data, _ := json.MarshalIndent(h, "", " ") + return os.WriteFile(bingHistoryPath(), data, 0644) +} + +func fetchBingHistory() { + bingMu.Lock() + defer bingMu.Unlock() + resp, err := httpClient.Get(bingAPI) if err != nil { log.Println("Bing API 请求失败:", err) @@ -37,38 +83,160 @@ func fetchBingWallpaper() { return } - imgURL := br.Images[0].URL - if !strings.HasPrefix(imgURL, "http") { - imgURL = "https://www.bing.com" + imgURL + os.MkdirAll(bingDir(), 0755) + + existing := loadBingHistory() + existingMap := make(map[string]BingRecord) + for _, r := range existing.Records { + existingMap[r.Date] = r } - imgResp, err := httpClient.Get(imgURL) - if err != nil { - log.Println("Bing 图片下载失败:", err) - return + for _, img := range br.Images { + date := img.StartDate + if date == "" { + continue + } + if _, exists := existingMap[date]; exists { + continue + } + + imgURL := img.URL + if !strings.HasPrefix(imgURL, "http") { + imgURL = "https://www.bing.com" + imgURL + } + + imgResp, err := httpClient.Get(imgURL) + if err != nil { + log.Printf("Bing 图片下载失败 (%s): %v", date, err) + continue + } + imgData, _ := io.ReadAll(imgResp.Body) + imgResp.Body.Close() + if len(imgData) == 0 { + continue + } + + filename := date + ".jpg" + localPath := filepath.Join(bingDir(), filename) + if err := os.WriteFile(localPath, imgData, 0644); err != nil { + log.Printf("Bing 图片保存失败 (%s): %v", date, err) + continue + } + log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData)) + + existingMap[date] = BingRecord{ + Date: date, + URLBase: img.URLBase, + Copyright: img.Copyright, + Filename: filename, + } } - defer imgResp.Body.Close() - imgData, err := io.ReadAll(imgResp.Body) - if err != nil { + + var records []BingRecord + for _, img := range br.Images { + if r, ok := existingMap[img.StartDate]; ok { + records = append(records, r) + } + } + if len(records) == 0 { return } - bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg") - if err := os.WriteFile(bingPath, imgData, 0644); err != nil { - log.Println("Bing 壁纸缓存失败:", err) - return + history := &BingHistory{ + Records: records, + CurrentIdx: 0, } - log.Printf("Bing 壁纸已缓存: %s (%d bytes)", bingPath, len(imgData)) + if existing.CurrentIdx < len(records) { + history.CurrentIdx = existing.CurrentIdx + } + saveBingHistory(history) reloadWallpaper() } +func getCurrentBingPath() string { + h := loadBingHistory() + if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { + return "" + } + return filepath.Join(bingDir(), h.Records[h.CurrentIdx].Filename) +} + +func getCurrentBingRecord() *BingRecord { + h := loadBingHistory() + if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { + return nil + } + return &h.Records[h.CurrentIdx] +} + +func bingPrev() { + bingMu.Lock() + defer bingMu.Unlock() + + h := loadBingHistory() + if len(h.Records) == 0 { + return + } + if h.CurrentIdx < len(h.Records)-1 { + h.CurrentIdx++ + saveBingHistory(h) + log.Printf("Bing 壁纸: 上一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date) + reloadWallpaper() + } +} + +func bingNext() { + bingMu.Lock() + defer bingMu.Unlock() + + h := loadBingHistory() + if h.CurrentIdx > 0 { + h.CurrentIdx-- + saveBingHistory(h) + log.Printf("Bing 壁纸: 下一个 (idx=%d, date=%s)", h.CurrentIdx, h.Records[h.CurrentIdx].Date) + reloadWallpaper() + } +} + +func bingToggleFavorite() string { + bingMu.Lock() + defer bingMu.Unlock() + + h := loadBingHistory() + if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) { + return "" + } + r := &h.Records[h.CurrentIdx] + r.Favorited = !r.Favorited + saveBingHistory(h) + state := "收藏" + if r.Favorited { + state = "取消收藏" + } + log.Printf("Bing 壁纸: %s (date=%s)", state, r.Date) + if r.Favorited { + return "☆ 取消收藏" + } + return "★ 收藏当前壁纸" +} + +func bingCopyrightInfo() string { + r := getCurrentBingRecord() + if r != nil { + return fmt.Sprintf("%s (%s)", r.Copyright, r.Date) + } + return "" +} + func bingWallpaperLoop() { cfg := loadConfig() if cfg.WallpaperType == WPBing { - bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg") - if _, err := os.Stat(bingPath); err != nil { - fetchBingWallpaper() + h := loadBingHistory() + if len(h.Records) == 0 { + fetchBingHistory() + } else { + reloadWallpaper() } } @@ -76,7 +244,7 @@ func bingWallpaperLoop() { for range ticker.C { cfg := loadConfig() if cfg.WallpaperType == WPBing { - fetchBingWallpaper() + fetchBingHistory() } } } diff --git a/config.go b/config.go index 79024ad..1c83b19 100644 --- a/config.go +++ b/config.go @@ -27,6 +27,8 @@ const ( ThemeStar ThemeName = "starfield" ThemeGradient ThemeName = "gradient" ThemeParticle ThemeName = "particles" + ThemeFractal ThemeName = "fractal" + ThemeText ThemeName = "text" ) type Config struct { @@ -38,6 +40,7 @@ type Config struct { Color1 string `json:"color1"` Color2 string `json:"color2"` ColorGradient bool `json:"colorGradient"` + WallpaperText string `json:"wallpaperText"` } const defaultZodiac = "射手座" diff --git a/systray.go b/systray.go index c8b39cc..0ccb2b9 100644 --- a/systray.go +++ b/systray.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "os/exec" "runtime" "strconv" "sync/atomic" @@ -26,6 +27,8 @@ var themeNames = []struct { {ThemeStar, "星空"}, {ThemeGradient, "渐变"}, {ThemeParticle, "粒子"}, + {ThemeFractal, "极光流体"}, + {ThemeText, "文字"}, } func onSystrayReady() { @@ -47,7 +50,11 @@ func onSystrayReady() { themeItems = append(themeItems, item) } mLocalImage := systray.AddMenuItem("本地图片", "选择本地图片作为壁纸") - mBingDaily := systray.AddMenuItem("Bing 每日壁纸", "使用 Bing 每日壁纸") + mBingMenu := systray.AddMenuItem("Bing 每日壁纸", "") + mBingEnable := mBingMenu.AddSubMenuItem("启用 Bing 壁纸", "") + mBingPrev := mBingMenu.AddSubMenuItem("◀ 上一个", "") + mBingNext := mBingMenu.AddSubMenuItem("下一个 ▶", "") + mBingFav := mBingMenu.AddSubMenuItem("★ 收藏当前壁纸", "") mSolidColor := systray.AddMenuItem("纯色壁纸", "选择纯色壁纸") mGradientColor := systray.AddMenuItem("渐变壁纸", "选择渐变壁纸") @@ -82,6 +89,7 @@ func onSystrayReady() { } systray.AddSeparator() + mRestart := systray.AddMenuItem("重启", "重启程序") mQuit := systray.AddMenuItem("退出", "退出程序") // 主题切换监听 @@ -123,10 +131,10 @@ func onSystrayReady() { } }() - // Bing 每日 + // Bing 启用 go func() { for { - <-mBingDaily.ClickedCh + <-mBingEnable.ClickedCh cfg := loadConfig() cfg.WallpaperType = WPBing saveConfig(cfg) @@ -134,7 +142,34 @@ func onSystrayReady() { it.Uncheck() } log.Println("切换 Bing 壁纸") - go fetchBingWallpaper() + 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) + } } }() @@ -243,6 +278,16 @@ func onSystrayReady() { os.Exit(0) }() + // 重启 + go func() { + for { + <-mRestart.ClickedCh + exe, _ := os.Executable() + exec.Command(exe).Start() + os.Exit(0) + } + }() + go startWebView() go weatherLoop() go bingWallpaperLoop() diff --git a/wallpaper.go b/wallpaper.go index 1dbc59d..7073f84 100644 --- a/wallpaper.go +++ b/wallpaper.go @@ -26,11 +26,19 @@ var themeGradient string //go:embed web/themes/particles.html var themeParticles string +//go:embed web/themes/fractal.html +var themeFractal string + +//go:embed web/themes/text.html +var themeText string + var themeMap = map[ThemeName]string{ ThemeAurora: themeAurora, ThemeStar: themeStarfield, ThemeGradient: themeGradient, ThemeParticle: themeParticles, + ThemeFractal: themeFractal, + ThemeText: themeText, } func buildWallpaperHTML(cfg *Config) string { @@ -51,11 +59,12 @@ func buildWallpaperHTML(cfg *Config) string { } } case WPBing: - bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg") - if _, err := os.Stat(bingPath); err == nil { - src := imageToDataURI(bingPath) - if src != "" { - bg = fmt.Sprintf(``, src) + if p := getCurrentBingPath(); p != "" { + if _, err := os.Stat(p); err == nil { + src := imageToDataURI(p) + if src != "" { + bg = fmt.Sprintf(``, src) + } } } case WPColor: @@ -69,7 +78,16 @@ func buildWallpaperHTML(cfg *Config) string { if bg == "" { bg = themeAurora } - return strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1) + html := strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1) + + // 注入自定义文字 + if cfg.WallpaperType == WPTheme && cfg.Theme == ThemeText && cfg.WallpaperText != "" { + escaped := strings.ReplaceAll(cfg.WallpaperText, `\`, `\\`) + escaped = strings.ReplaceAll(escaped, `"`, `\"`) + html = strings.Replace(html, "", `window.wallpaperText = "`+escaped+`";`, 1) + } + + return html } func imageToDataURI(path string) string { @@ -107,6 +125,9 @@ func reloadWallpaper() { go func() { time.Sleep(1 * time.Second) evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac)) + if cfg.Theme == ThemeText && cfg.WallpaperText != "" { + evalJS(fmt.Sprintf(`window.wallpaperText = %q; var el=document.getElementById("wallpaper-text"); if(el){el.textContent=%q;}`, cfg.WallpaperText, cfg.WallpaperText)) + } city := getCurrentCity() go fetchAndPushWeather(city) }() diff --git a/web/overlay.html b/web/overlay.html index 59ce403..e267da0 100644 --- a/web/overlay.html +++ b/web/overlay.html @@ -1,3 +1,4 @@ + @@ -13,37 +14,120 @@ html, body { #info { position: fixed; - top: 50%; - right: 80px; - transform: translateY(-50%); - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(20px); - border-radius: 16px; - padding: 24px 32px; + top: 40px; + right: 40px; + background: rgba(0, 0, 0, 0.25); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-radius: 20px; + padding: 28px 32px; color: #ffffff; font-family: "Microsoft YaHei", sans-serif; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255,255,255,0.08); text-align: right; - min-width: 300px; - max-height: 90vh; - overflow-y: auto; + min-width: 320px; z-index: 10; } -.time { font-size: 64px; font-weight: 300; color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,0.8); letter-spacing: -1px; } -.date { font-size: 15px; color: rgba(255,255,255,0.9); margin-top: 6px; text-shadow: 0 1px 6px rgba(0,0,0,0.8); } -.weather-section { margin-top: 20px; } -.current-weather { font-size: 17px; color: rgba(255,255,255,0.95); text-shadow: 0 1px 4px rgba(0,0,0,0.8); margin-bottom: 12px; } -.forecast-title { font-size: 13px; color: rgba(255,255,255,0.7); margin-bottom: 8px; } -.weather-forecast { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 8px; justify-content: flex-end; } -.forecast-item { background: rgba(255,255,255,0.08); border-radius: 8px; padding: 8px 10px; text-align: center; min-width: 55px; font-size: 11px; color: rgba(255,255,255,0.85); } -.forecast-time { margin-bottom: 4px; opacity: 0.8; } -.forecast-temp { font-weight: 500; } -.daily-forecast { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 8px; justify-content: flex-end; } -.daily-item { background: rgba(255,255,255,0.06); border-radius: 6px; padding: 6px 8px; text-align: center; min-width: 48px; font-size: 11px; color: rgba(255,255,255,0.85); } -.zodiac { margin-top: 18px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.15); font-size: 15px; color: rgba(255,255,255,0.9); line-height: 1.5; text-shadow: 0 1px 4px rgba(0,0,0,0.8); } -.weather-forecast::-webkit-scrollbar, #info::-webkit-scrollbar { display: none; } +.time { + font-size: 72px; + font-weight: 200; + color: #fff; + text-shadow: 0 2px 20px rgba(0,0,0,0.5); + letter-spacing: -2px; + line-height: 1; +} +.date { + font-size: 14px; + font-weight: 400; + color: rgba(255,255,255,0.7); + margin-top: 8px; + text-shadow: 0 1px 6px rgba(0,0,0,0.5); + letter-spacing: 0.5px; +} + +.divider { + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent); + margin: 16px 0; +} + +.weather-section {} +.current-weather { + font-size: 16px; + font-weight: 500; + color: rgba(255,255,255,0.95); + text-shadow: 0 1px 4px rgba(0,0,0,0.5); + margin-bottom: 14px; +} +.forecast-title { + font-size: 11px; + font-weight: 500; + color: rgba(255,255,255,0.45); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 8px; +} +.weather-forecast { + display: flex; + gap: 4px; + overflow-x: auto; + padding-bottom: 6px; + justify-content: flex-end; +} +.forecast-item { + background: rgba(255,255,255,0.06); + border-radius: 10px; + padding: 8px 10px; + text-align: center; + min-width: 52px; + font-size: 11px; + color: rgba(255,255,255,0.8); + border: 1px solid rgba(255,255,255,0.04); +} +.forecast-time { margin-bottom: 4px; opacity: 0.6; font-size: 10px; } +.forecast-temp { font-weight: 500; margin-top: 2px; } + +.daily-forecast { + display: flex; + gap: 4px; + overflow-x: auto; + padding-bottom: 6px; + justify-content: flex-end; +} +.daily-item { + background: rgba(255,255,255,0.04); + border-radius: 8px; + padding: 6px 8px; + text-align: center; + min-width: 48px; + font-size: 11px; + color: rgba(255,255,255,0.8); + border: 1px solid rgba(255,255,255,0.03); +} + +.zodiac { + font-size: 14px; + color: rgba(255,255,255,0.9); + line-height: 1.6; + text-shadow: 0 1px 4px rgba(0,0,0,0.5); +} + +.weather-forecast::-webkit-scrollbar, +.daily-forecast::-webkit-scrollbar, +#info::-webkit-scrollbar { display: none; } + +#author { + position: fixed; + bottom: 60px; + right: 30px; + font-size: 16px; + color: rgba(255,255,255,0.5); + font-family: "Microsoft YaHei", sans-serif; + letter-spacing: 1px; + z-index: 10; + pointer-events: none; +} @@ -52,16 +136,20 @@ html, body {
00:00
1月1日 周一
+
-
🌤️ 加载中...
-
未来24小时
+
加载中...
+
24小时预报
-
未来7天
+
7日预报
-
✨ 射手座运势
+
+
加载中...
+
绝尘
+ diff --git a/web/themes/text.html b/web/themes/text.html new file mode 100644 index 0000000..f567c45 --- /dev/null +++ b/web/themes/text.html @@ -0,0 +1,37 @@ + +
+