新增: Bing 壁纸多市场获取/2000张历史/第三方图源/dislike/重下/方向修复
This commit is contained in:
@@ -13,15 +13,26 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const bingAPIBase = "https://www.bing.com/HPImageArchive.aspx?format=js&n=8&mkt=zh-CN"
|
const bingAPIPrefix = "https://www.bing.com/HPImageArchive.aspx?format=js&n=8"
|
||||||
|
|
||||||
|
type bingImage struct {
|
||||||
|
StartDate string `json:"startdate"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
URLBase string `json:"urlbase"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
}
|
||||||
|
|
||||||
type bingResponse struct {
|
type bingResponse struct {
|
||||||
Images []struct {
|
Images []bingImage `json:"images"`
|
||||||
StartDate string `json:"startdate"`
|
}
|
||||||
URL string `json:"url"`
|
|
||||||
URLBase string `json:"urlbase"`
|
type biturlResponse struct {
|
||||||
Copyright string `json:"copyright"`
|
StartDate string `json:"start_date"`
|
||||||
} `json:"images"`
|
EndDate string `json:"end_date"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
CopyrightLink string `json:"copyright_link"`
|
||||||
|
Resolution string `json:"resolution"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BingRecord struct {
|
type BingRecord struct {
|
||||||
@@ -30,8 +41,33 @@ type BingRecord struct {
|
|||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Favorited bool `json:"favorited"`
|
Favorited bool `json:"favorited"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
|
Market string `json:"market"`
|
||||||
|
Source string `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxBingRecords = 2000
|
||||||
|
|
||||||
|
var bingMarkets = []string{
|
||||||
|
"zh-CN", "en-US", "ja-JP", "de-DE", "fr-FR",
|
||||||
|
"en-GB", "ko-KR", "ru-RU", "pt-BR", "es-ES",
|
||||||
|
}
|
||||||
|
|
||||||
|
var bingMarketLabels = map[string]string{
|
||||||
|
"zh-CN": "中国",
|
||||||
|
"en-US": "美国",
|
||||||
|
"ja-JP": "日本",
|
||||||
|
"de-DE": "德国",
|
||||||
|
"fr-FR": "法国",
|
||||||
|
"en-GB": "英国",
|
||||||
|
"ko-KR": "韩国",
|
||||||
|
"ru-RU": "俄罗斯",
|
||||||
|
"pt-BR": "巴西",
|
||||||
|
"es-ES": "西班牙",
|
||||||
|
}
|
||||||
|
|
||||||
|
const bingBiturlAPI = "https://bing.biturl.top/?format=json"
|
||||||
|
|
||||||
type BingHistory struct {
|
type BingHistory struct {
|
||||||
Records []BingRecord `json:"records"`
|
Records []BingRecord `json:"records"`
|
||||||
CurrentIdx int `json:"currentIdx"`
|
CurrentIdx int `json:"currentIdx"`
|
||||||
@@ -60,91 +96,249 @@ func loadBingHistory() *BingHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveBingHistory(h *BingHistory) error {
|
func saveBingHistory(h *BingHistory) error {
|
||||||
data, _ := json.MarshalIndent(h, "", " ")
|
data, err := json.MarshalIndent(h, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Bing 历史序列化失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return os.WriteFile(bingHistoryPath(), data, 0644)
|
return os.WriteFile(bingHistoryPath(), data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bingKey builds a composite dedup key: market + "_" + date
|
||||||
|
func bingKey(market, date string) string {
|
||||||
|
if market == "" {
|
||||||
|
return "_" + date
|
||||||
|
}
|
||||||
|
return market + "_" + date
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadBingImage downloads a single Bing image from the record's URLBase
|
||||||
|
func downloadBingImage(r *BingRecord) error {
|
||||||
|
imgURL := r.URLBase
|
||||||
|
if imgURL == "" {
|
||||||
|
return fmt.Errorf("empty URLBase for %s", r.Filename)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(imgURL, "http") {
|
||||||
|
imgURL = "https://www.bing.com" + imgURL
|
||||||
|
}
|
||||||
|
// For official Bing source, ensure resolution suffix
|
||||||
|
if r.Source == "bing_official" || r.Source == "" {
|
||||||
|
if !strings.Contains(imgURL, "_1920x1080") && !strings.HasSuffix(imgURL, ".jpg") {
|
||||||
|
imgURL += "_1920x1080.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgResp, err := httpClient.Get(imgURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("http get failed: %w", err)
|
||||||
|
}
|
||||||
|
defer imgResp.Body.Close()
|
||||||
|
imgData, err := io.ReadAll(imgResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read body failed: %w", err)
|
||||||
|
}
|
||||||
|
if len(imgData) == 0 {
|
||||||
|
return fmt.Errorf("empty response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
localPath := filepath.Join(bingDir(), r.Filename)
|
||||||
|
return os.WriteFile(localPath, imgData, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchBingHistory fetches Bing wallpapers from multiple markets and sources.
|
||||||
|
// Phase 1: Official Bing API per market (idx=0,8,...192)
|
||||||
|
// Phase 2: biturl.top supplement per market (5 attempts each)
|
||||||
|
// Phase 3: Enforce maxBingRecords cap
|
||||||
func fetchBingHistory() {
|
func fetchBingHistory() {
|
||||||
bingMu.Lock()
|
bingMu.Lock()
|
||||||
defer bingMu.Unlock()
|
defer bingMu.Unlock()
|
||||||
|
|
||||||
os.MkdirAll(bingDir(), 0755)
|
os.MkdirAll(bingDir(), 0755)
|
||||||
existing := loadBingHistory()
|
existing := loadBingHistory()
|
||||||
|
|
||||||
|
// Build composite-key map from existing records
|
||||||
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[bingKey(r.Market, r.Date)] = r
|
||||||
|
// Migration: old records with Market="" were from zh-CN single market
|
||||||
|
if r.Market == "" {
|
||||||
|
existingMap[bingKey("zh-CN", r.Date)] = r
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页下载: idx=0,8,16... 直到命中已有记录或无新图片
|
cfg := loadConfig()
|
||||||
for idx := 0; idx < 80; idx += 8 {
|
markets := cfg.BingMarkets
|
||||||
url := fmt.Sprintf("%s&idx=%d", bingAPIBase, idx)
|
if len(markets) == 0 {
|
||||||
resp, err := httpClient.Get(url)
|
markets = bingMarkets
|
||||||
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
|
// track new records per key for dedup across phases
|
||||||
for _, img := range br.Images {
|
newRecords := make(map[string]BingRecord)
|
||||||
date := img.StartDate
|
|
||||||
if date == "" {
|
// ---- Phase 1: Official Bing API per market ----
|
||||||
|
for _, market := range markets {
|
||||||
|
for idx := 0; idx < 192; idx += 8 {
|
||||||
|
url := fmt.Sprintf("%s&mkt=%s&idx=%d", bingAPIPrefix, market, idx)
|
||||||
|
resp, err := httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Bing API 请求失败 (market=%s, idx=%d): %v", market, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNew := 0
|
||||||
|
for _, img := range br.Images {
|
||||||
|
date := img.StartDate
|
||||||
|
if date == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := bingKey(market, date)
|
||||||
|
if _, exists := existingMap[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := newRecords[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the image
|
||||||
|
imgURL := img.URL
|
||||||
|
if !strings.HasPrefix(imgURL, "http") {
|
||||||
|
imgURL = "https://www.bing.com" + imgURL
|
||||||
|
}
|
||||||
|
|
||||||
|
imgResp, err := httpClient.Get(imgURL)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imgData, _ := io.ReadAll(imgResp.Body)
|
||||||
|
imgResp.Body.Close()
|
||||||
|
if len(imgData) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%s_%s.jpg", market, date)
|
||||||
|
localPath := filepath.Join(bingDir(), filename)
|
||||||
|
os.WriteFile(localPath, imgData, 0644)
|
||||||
|
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
|
||||||
|
|
||||||
|
newRecords[key] = BingRecord{
|
||||||
|
Date: date,
|
||||||
|
URLBase: img.URLBase,
|
||||||
|
Copyright: img.Copyright,
|
||||||
|
Filename: filename,
|
||||||
|
Market: market,
|
||||||
|
Source: "bing_official",
|
||||||
|
}
|
||||||
|
pageNew++
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageNew == 0 && idx > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Phase 2: biturl.top supplement ----
|
||||||
|
for _, market := range markets {
|
||||||
|
for attempt := 0; attempt < 5; attempt++ {
|
||||||
|
url := fmt.Sprintf("%s&mkt=%s", bingBiturlAPI, market)
|
||||||
|
resp, err := httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Bing biturl 请求失败 (market=%s): %v", market, err)
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := existingMap[date]; exists {
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
var bit biturlResponse
|
||||||
|
if json.Unmarshal(data, &bit) != nil || bit.StartDate == "" {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
imgURL := img.URL
|
date := bit.StartDate
|
||||||
if !strings.HasPrefix(imgURL, "http") {
|
key := bingKey(market, date)
|
||||||
imgURL = "https://www.bing.com" + imgURL
|
if _, exists := existingMap[key]; exists {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := newRecords[key]; exists {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download from biturl URL
|
||||||
|
imgURL := bit.URL
|
||||||
imgResp, err := httpClient.Get(imgURL)
|
imgResp, err := httpClient.Get(imgURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
imgData, _ := io.ReadAll(imgResp.Body)
|
imgData, _ := io.ReadAll(imgResp.Body)
|
||||||
imgResp.Body.Close()
|
imgResp.Body.Close()
|
||||||
if len(imgData) == 0 {
|
if len(imgData) == 0 {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := date + ".jpg"
|
filename := fmt.Sprintf("%s_%s.jpg", market, date)
|
||||||
localPath := filepath.Join(bingDir(), filename)
|
localPath := filepath.Join(bingDir(), filename)
|
||||||
os.WriteFile(localPath, imgData, 0644)
|
os.WriteFile(localPath, imgData, 0644)
|
||||||
log.Printf("Bing 壁纸已下载: %s (%d bytes)", filename, len(imgData))
|
log.Printf("Bing biturl 壁纸已下载: %s (%d bytes)", filename, len(imgData))
|
||||||
|
|
||||||
existingMap[date] = BingRecord{
|
newRecords[key] = BingRecord{
|
||||||
Date: date,
|
Date: date,
|
||||||
URLBase: img.URLBase,
|
URLBase: bit.URL,
|
||||||
Copyright: img.Copyright,
|
Copyright: bit.Copyright,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
|
Market: market,
|
||||||
|
Source: "bing_biturl",
|
||||||
}
|
}
|
||||||
newCount++
|
time.Sleep(300 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newCount == 0 && idx > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 API 返回顺序重建 records (newest first)
|
// Merge existing + new records
|
||||||
// 用 existingMap 里所有记录按 date 降序排列
|
allMap := make(map[string]BingRecord)
|
||||||
|
for k, r := range existingMap {
|
||||||
|
allMap[k] = r
|
||||||
|
}
|
||||||
|
for k, r := range newRecords {
|
||||||
|
allMap[k] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date descending, then by market for determinism
|
||||||
var records []BingRecord
|
var records []BingRecord
|
||||||
for _, r := range existingMap {
|
for _, r := range allMap {
|
||||||
records = append(records, r)
|
records = append(records, r)
|
||||||
}
|
}
|
||||||
sort.Slice(records, func(i, j int) bool {
|
sort.Slice(records, func(i, j int) bool {
|
||||||
return records[i].Date > records[j].Date
|
if records[i].Date != records[j].Date {
|
||||||
|
return records[i].Date > records[j].Date
|
||||||
|
}
|
||||||
|
return records[i].Market < records[j].Market
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- Phase 3: Enforce 2000 cap ----
|
||||||
|
if len(records) > maxBingRecords {
|
||||||
|
// Remove oldest records and their files
|
||||||
|
toRemove := records[maxBingRecords:]
|
||||||
|
records = records[:maxBingRecords]
|
||||||
|
for _, r := range toRemove {
|
||||||
|
p := filepath.Join(bingDir(), r.Filename)
|
||||||
|
os.Remove(p)
|
||||||
|
}
|
||||||
|
log.Printf("Bing 壁纸: 裁剪到 %d 张, 删除 %d 张旧图", maxBingRecords, len(toRemove))
|
||||||
|
}
|
||||||
|
|
||||||
history := &BingHistory{
|
history := &BingHistory{
|
||||||
Records: records,
|
Records: records,
|
||||||
CurrentIdx: 0,
|
CurrentIdx: 0,
|
||||||
@@ -154,7 +348,7 @@ func fetchBingHistory() {
|
|||||||
}
|
}
|
||||||
saveBingHistory(history)
|
saveBingHistory(history)
|
||||||
|
|
||||||
log.Printf("Bing 壁纸: 共 %d 张", len(records))
|
log.Printf("Bing 壁纸: 共 %d 张 (新增 %d 张)", len(records), len(newRecords))
|
||||||
reloadWallpaper()
|
reloadWallpaper()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +357,18 @@ func getCurrentBingPath() string {
|
|||||||
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
|
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return filepath.Join(bingDir(), h.Records[h.CurrentIdx].Filename)
|
r := &h.Records[h.CurrentIdx]
|
||||||
|
p := filepath.Join(bingDir(), r.Filename)
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// File missing, try re-download
|
||||||
|
log.Printf("Bing 壁纸文件缺失, 尝试重新下载: %s", r.Filename)
|
||||||
|
if err := downloadBingImage(r); err != nil {
|
||||||
|
log.Printf("Bing 重新下载失败: %s, %v", r.Filename, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentBingRecord() *BingRecord {
|
func getCurrentBingRecord() *BingRecord {
|
||||||
@@ -174,6 +379,7 @@ func getCurrentBingRecord() *BingRecord {
|
|||||||
return &h.Records[h.CurrentIdx]
|
return &h.Records[h.CurrentIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bingPrev moves to the previous (older) image, skipping hidden ones.
|
||||||
func bingPrev() {
|
func bingPrev() {
|
||||||
bingMu.Lock()
|
bingMu.Lock()
|
||||||
defer bingMu.Unlock()
|
defer bingMu.Unlock()
|
||||||
@@ -182,12 +388,23 @@ func bingPrev() {
|
|||||||
if len(h.Records) == 0 {
|
if len(h.Records) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
|
attempts := 0
|
||||||
|
for {
|
||||||
|
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
|
||||||
|
if !h.Records[h.CurrentIdx].Hidden {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
if attempts >= len(h.Records) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
bingReloadImage()
|
bingReloadImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bingNext moves to the next (newer) image, skipping hidden ones.
|
||||||
func bingNext() {
|
func bingNext() {
|
||||||
bingMu.Lock()
|
bingMu.Lock()
|
||||||
defer bingMu.Unlock()
|
defer bingMu.Unlock()
|
||||||
@@ -196,7 +413,17 @@ func bingNext() {
|
|||||||
if len(h.Records) == 0 {
|
if len(h.Records) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
|
attempts := 0
|
||||||
|
for {
|
||||||
|
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
|
||||||
|
if !h.Records[h.CurrentIdx].Hidden {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
if attempts >= len(h.Records) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
bingReloadImage()
|
bingReloadImage()
|
||||||
@@ -221,10 +448,46 @@ func bingToggleFavorite() string {
|
|||||||
return "★ 收藏当前壁纸"
|
return "★ 收藏当前壁纸"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bingToggleDislike toggles the hidden flag on the current record.
|
||||||
|
// When hidden, auto-advances to the next visible image.
|
||||||
|
func bingToggleDislike() string {
|
||||||
|
bingMu.Lock()
|
||||||
|
defer bingMu.Unlock()
|
||||||
|
|
||||||
|
h := loadBingHistory()
|
||||||
|
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
|
||||||
|
return bingCurrentState()
|
||||||
|
}
|
||||||
|
r := &h.Records[h.CurrentIdx]
|
||||||
|
r.Hidden = !r.Hidden
|
||||||
|
saveBingHistory(h)
|
||||||
|
|
||||||
|
if r.Hidden {
|
||||||
|
log.Printf("Bing 壁纸: 已隐藏 (date=%s)", r.Date)
|
||||||
|
// Advance to next visible
|
||||||
|
attempts := 0
|
||||||
|
for {
|
||||||
|
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
|
||||||
|
if !h.Records[h.CurrentIdx].Hidden {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
if attempts >= len(h.Records) {
|
||||||
|
return bingCurrentState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveBingHistory(h)
|
||||||
|
bingReloadImage()
|
||||||
|
return bingCurrentState()
|
||||||
|
}
|
||||||
|
log.Printf("Bing 壁纸: 已取消隐藏 (date=%s)", r.Date)
|
||||||
|
return bingCurrentState()
|
||||||
|
}
|
||||||
|
|
||||||
func bingCurrentState() string {
|
func bingCurrentState() string {
|
||||||
h := loadBingHistory()
|
h := loadBingHistory()
|
||||||
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
|
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
|
||||||
return `{"fav":false,"label":"☆","copyright":"","idx":0,"total":0}`
|
return `{"fav":false,"label":"☆","copyright":"","idx":0,"total":0,"hidden":false}`
|
||||||
}
|
}
|
||||||
r := h.Records[h.CurrentIdx]
|
r := h.Records[h.CurrentIdx]
|
||||||
label := "☆"
|
label := "☆"
|
||||||
@@ -239,6 +502,8 @@ func bingCurrentState() string {
|
|||||||
"filename": r.Filename,
|
"filename": r.Filename,
|
||||||
"idx": h.CurrentIdx,
|
"idx": h.CurrentIdx,
|
||||||
"total": len(h.Records),
|
"total": len(h.Records),
|
||||||
|
"hidden": r.Hidden,
|
||||||
|
"market": r.Market,
|
||||||
})
|
})
|
||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
@@ -253,7 +518,7 @@ func bingFavoritesJSON() string {
|
|||||||
}
|
}
|
||||||
var favs []favItem
|
var favs []favItem
|
||||||
for i, r := range h.Records {
|
for i, r := range h.Records {
|
||||||
if r.Favorited {
|
if r.Favorited && !r.Hidden {
|
||||||
favs = append(favs, favItem{r.Date, r.Copyright, r.Filename, i})
|
favs = append(favs, favItem{r.Date, r.Copyright, r.Filename, i})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,6 +544,18 @@ func bingSetByIdx(idx int) string {
|
|||||||
|
|
||||||
func bingThumbDataURI(filename string) string {
|
func bingThumbDataURI(filename string) string {
|
||||||
p := filepath.Join(bingDir(), filename)
|
p := filepath.Join(bingDir(), filename)
|
||||||
|
// Auto redownload if missing
|
||||||
|
if _, err := os.Stat(p); err != nil {
|
||||||
|
// Try to find the record and re-download
|
||||||
|
h := loadBingHistory()
|
||||||
|
for _, r := range h.Records {
|
||||||
|
if r.Filename == filename {
|
||||||
|
log.Printf("Bing 缩略图文件缺失, 尝试重新下载: %s", filename)
|
||||||
|
downloadBingImage(&r)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return imageToDataURI(p)
|
return imageToDataURI(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +570,7 @@ func bingWallpaperLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定时切换壁纸 (1 小时间隔)
|
// Auto-refresh: cycle through images (newest direction)
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(1 * time.Hour)
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
@@ -307,7 +584,18 @@ func bingWallpaperLoop() {
|
|||||||
bingMu.Unlock()
|
bingMu.Unlock()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
|
// Move to next (newer) image, skip hidden
|
||||||
|
attempts := 0
|
||||||
|
for {
|
||||||
|
h.CurrentIdx = (h.CurrentIdx - 1 + len(h.Records)) % len(h.Records)
|
||||||
|
if !h.Records[h.CurrentIdx].Hidden {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
if attempts >= len(h.Records) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
saveBingHistory(h)
|
saveBingHistory(h)
|
||||||
bingMu.Unlock()
|
bingMu.Unlock()
|
||||||
bingReloadImage()
|
bingReloadImage()
|
||||||
@@ -315,6 +603,7 @@ func bingWallpaperLoop() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Periodic fetch
|
||||||
go func() {
|
go func() {
|
||||||
fetchTicker := time.NewTicker(4 * time.Hour)
|
fetchTicker := time.NewTicker(4 * time.Hour)
|
||||||
for range fetchTicker.C {
|
for range fetchTicker.C {
|
||||||
@@ -324,4 +613,4 @@ func bingWallpaperLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,7 @@ type Config struct {
|
|||||||
HideKnowledge bool `json:"hideKnowledge"`
|
HideKnowledge bool `json:"hideKnowledge"`
|
||||||
SavedColors []SavedColor `json:"savedColors"`
|
SavedColors []SavedColor `json:"savedColors"`
|
||||||
BingAutoRefresh bool `json:"bingAutoRefresh"`
|
BingAutoRefresh bool `json:"bingAutoRefresh"`
|
||||||
|
BingMarkets []string `json:"bingMarkets,omitempty"`
|
||||||
AutoStart bool `json:"autoStart"`
|
AutoStart bool `json:"autoStart"`
|
||||||
QWeatherKey string `json:"qweatherKey,omitempty"`
|
QWeatherKey string `json:"qweatherKey,omitempty"`
|
||||||
TianapiKey string `json:"tianapiKey,omitempty"`
|
TianapiKey string `json:"tianapiKey,omitempty"`
|
||||||
@@ -116,6 +117,9 @@ func loadConfig() *Config {
|
|||||||
if cfg.Layout == "" {
|
if cfg.Layout == "" {
|
||||||
cfg.Layout = LayoutSingle
|
cfg.Layout = LayoutSingle
|
||||||
}
|
}
|
||||||
|
if len(cfg.BingMarkets) == 0 {
|
||||||
|
cfg.BingMarkets = []string{"zh-CN", "en-US", "ja-JP", "de-DE", "fr-FR", "en-GB", "ko-KR", "ru-RU", "pt-BR", "es-ES"}
|
||||||
|
}
|
||||||
return &cfg
|
return &cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
@@ -390,6 +390,23 @@ func openSettingsWindow() {
|
|||||||
w.Bind("bingThumbDataURI", func(filename string) string {
|
w.Bind("bingThumbDataURI", func(filename string) string {
|
||||||
return bingThumbDataURI(filename)
|
return bingThumbDataURI(filename)
|
||||||
})
|
})
|
||||||
|
w.Bind("bingToggleDislike", func() string {
|
||||||
|
return bingToggleDislike()
|
||||||
|
})
|
||||||
|
w.Bind("getBingMarkets", func() string {
|
||||||
|
cfg := loadConfig()
|
||||||
|
data, _ := json.Marshal(cfg.BingMarkets)
|
||||||
|
return string(data)
|
||||||
|
})
|
||||||
|
w.Bind("saveBingMarkets", func(marketsJSON string) string {
|
||||||
|
var markets []string
|
||||||
|
if json.Unmarshal([]byte(marketsJSON), &markets) == nil {
|
||||||
|
cfg := loadConfig()
|
||||||
|
cfg.BingMarkets = markets
|
||||||
|
saveConfig(cfg)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
w.Bind("pickSolidColor", func() string {
|
w.Bind("pickSolidColor", func() string {
|
||||||
hwnd := uintptr(w.Window())
|
hwnd := uintptr(w.Window())
|
||||||
|
|||||||
@@ -91,11 +91,25 @@
|
|||||||
<button class="btn-sm" @click="onBingPrev">◀</button>
|
<button class="btn-sm" @click="onBingPrev">◀</button>
|
||||||
<button class="btn-sm" @click="onBingNext">▶</button>
|
<button class="btn-sm" @click="onBingNext">▶</button>
|
||||||
<button class="btn-sm" @click="onBingFav">{{ bing.fav ? '★' : '☆' }}</button>
|
<button class="btn-sm" @click="onBingFav">{{ bing.fav ? '★' : '☆' }}</button>
|
||||||
|
<button class="btn-sm" @click="onBingDislike">👎</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
|
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
|
||||||
<div><div class="text-xs font-medium text-[var(--text-muted)]">定时切换</div><div class="text-[10px] text-[var(--text-weak)] mt-px">每小时自动切换壁纸</div></div>
|
<div><div class="text-xs font-medium text-[var(--text-muted)]">定时切换</div><div class="text-[10px] text-[var(--text-weak)] mt-px">每小时自动切换壁纸</div></div>
|
||||||
<ToggleSwitch v-model="s.bingAutoRefresh" />
|
<ToggleSwitch v-model="s.bingAutoRefresh" />
|
||||||
|
</div>
|
||||||
|
<!-- 市场选择 -->
|
||||||
|
<div class="px-3.5 py-2 border-t border-[var(--card-divider)]">
|
||||||
|
<div class="flex justify-between items-center cursor-pointer select-none" @click="showMarkets = !showMarkets">
|
||||||
|
<div class="text-xs font-medium text-[var(--text-muted)]">壁纸来源地区</div>
|
||||||
|
<span class="text-[10px] text-[var(--text-weak)]">{{ showMarkets ? '收起' : '展开' }} {{ selectedMarkets.length }}/{{ allMarkets.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="showMarkets" class="flex flex-wrap gap-2 mt-2">
|
||||||
|
<label v-for="m in allMarkets" :key="m.code" class="flex items-center gap-1.5 text-xs text-[var(--text)] cursor-pointer">
|
||||||
|
<input type="checkbox" :checked="selectedMarkets.includes(m.code)" @change="toggleMarket(m.code)" class="accent-[var(--accent)]">
|
||||||
|
{{ m.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 收藏列表 -->
|
<!-- 收藏列表 -->
|
||||||
<div v-if="bingFavs.length" class="px-3.5 py-2 border-t border-[var(--card-divider)]">
|
<div v-if="bingFavs.length" class="px-3.5 py-2 border-t border-[var(--card-divider)]">
|
||||||
@@ -185,9 +199,12 @@ const audioColors = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// Bing
|
// Bing
|
||||||
const bing = ref<BingState>({ date: '', title: '', copyright: '', url: '', fav: false, idx: 0, total: 0 })
|
const bing = ref<BingState>({ date: '', title: '', copyright: '', url: '', fav: false, idx: 0, total: 0, hidden: false, market: '' })
|
||||||
const bingThumb = ref('')
|
const bingThumb = ref('')
|
||||||
const bingFavs = ref<{ idx: number; thumb: string }[]>([])
|
const bingFavs = ref<{ idx: number; thumb: string }[]>([])
|
||||||
|
const showMarkets = ref(false)
|
||||||
|
const allMarkets = ref<{ code: string; label: string }[]>([])
|
||||||
|
const selectedMarkets = ref<string[]>([])
|
||||||
|
|
||||||
async function loadBingInfo() {
|
async function loadBingInfo() {
|
||||||
const raw = await go.getBingInfo()
|
const raw = await go.getBingInfo()
|
||||||
@@ -199,23 +216,53 @@ async function loadBingInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBingPrev() { bing.value = JSON.parse(await go.bingPrev()) }
|
async function loadBingMarkets() {
|
||||||
|
const raw = await go.getBingMarkets()
|
||||||
|
const codes = JSON.parse(raw) as string[]
|
||||||
|
selectedMarkets.value = codes
|
||||||
|
allMarkets.value = [
|
||||||
|
{ code: 'zh-CN', label: '中国' },
|
||||||
|
{ code: 'en-US', label: '美国' },
|
||||||
|
{ code: 'ja-JP', label: '日本' },
|
||||||
|
{ code: 'de-DE', label: '德国' },
|
||||||
|
{ code: 'fr-FR', label: '法国' },
|
||||||
|
{ code: 'en-GB', label: '英国' },
|
||||||
|
{ code: 'ko-KR', label: '韩国' },
|
||||||
|
{ code: 'ru-RU', label: '俄罗斯' },
|
||||||
|
{ code: 'pt-BR', label: '巴西' },
|
||||||
|
{ code: 'es-ES', label: '西班牙' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMarket(code: string) {
|
||||||
|
const idx = selectedMarkets.value.indexOf(code)
|
||||||
|
if (idx >= 0) {
|
||||||
|
selectedMarkets.value.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
selectedMarkets.value.push(code)
|
||||||
|
}
|
||||||
|
go.saveBingMarkets(JSON.stringify(selectedMarkets.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左 ◄ = 上一张(更旧), 右 ► = 下一张(更新)
|
||||||
async function onBingNext() { bing.value = JSON.parse(await go.bingNext()) }
|
async function onBingNext() { bing.value = JSON.parse(await go.bingNext()) }
|
||||||
|
async function onBingPrev() { bing.value = JSON.parse(await go.bingPrev()) }
|
||||||
async function onBingFav() { bing.value = JSON.parse(await go.bingToggleFavorite()); await loadBingFavs() }
|
async function onBingFav() { bing.value = JSON.parse(await go.bingToggleFavorite()); await loadBingFavs() }
|
||||||
async function onBingSetByIdx(idx: number) { bing.value = JSON.parse(await go.bingSetByIdx(idx)) }
|
async function onBingSetByIdx(idx: number) { bing.value = JSON.parse(await go.bingSetByIdx(idx)) }
|
||||||
|
async function onBingDislike() { bing.value = JSON.parse(await go.bingToggleDislike()) }
|
||||||
|
|
||||||
async function loadBingFavs() {
|
async function loadBingFavs() {
|
||||||
const raw = await go.getBingFavorites()
|
const raw = await go.getBingFavorites()
|
||||||
const list = JSON.parse(raw) as string[]
|
const list = JSON.parse(raw) as { date: string; copyright: string; filename: string; idx: number }[]
|
||||||
bingFavs.value = await Promise.all(list.map(async f => ({
|
bingFavs.value = await Promise.all(list.map(async f => ({
|
||||||
idx: parseInt(f.match(/(\d+)/)?.[1] || '0', 10),
|
idx: f.idx,
|
||||||
thumb: await go.bingThumbDataURI(f)
|
thumb: await go.bingThumbDataURI(f.filename)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.s.bingAutoRefresh, v => go.saveBingAutoRefresh(v))
|
watch(() => props.s.bingAutoRefresh, v => go.saveBingAutoRefresh(v))
|
||||||
watch(() => props.s.wallpaperType, t => {
|
watch(() => props.s.wallpaperType, t => {
|
||||||
if (t === 'bing') { loadBingInfo(); loadBingFavs() }
|
if (t === 'bing') { loadBingInfo(); loadBingFavs(); loadBingMarkets() }
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
async function onPickImage() {
|
async function onPickImage() {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export const go = {
|
|||||||
getBingFavorites: (): Promise<string> => w.getBingFavorites(),
|
getBingFavorites: (): Promise<string> => w.getBingFavorites(),
|
||||||
bingSetByIdx: (idx: number): Promise<string> => w.bingSetByIdx(idx),
|
bingSetByIdx: (idx: number): Promise<string> => w.bingSetByIdx(idx),
|
||||||
bingThumbDataURI: (filename: string): Promise<string> => w.bingThumbDataURI(filename),
|
bingThumbDataURI: (filename: string): Promise<string> => w.bingThumbDataURI(filename),
|
||||||
|
bingToggleDislike: (): Promise<string> => w.bingToggleDislike(),
|
||||||
|
getBingMarkets: (): Promise<string> => w.getBingMarkets(),
|
||||||
|
saveBingMarkets: (markets: string) => w.saveBingMarkets(markets),
|
||||||
pickSolidColor: (): Promise<string> => w.pickSolidColor(),
|
pickSolidColor: (): Promise<string> => w.pickSolidColor(),
|
||||||
pickGradientColor: (): Promise<string> => w.pickGradientColor(),
|
pickGradientColor: (): Promise<string> => w.pickGradientColor(),
|
||||||
addSavedColor: (c1: string, c2: string, gradient: boolean) => w.addSavedColor(c1, c2, gradient),
|
addSavedColor: (c1: string, c2: string, gradient: boolean) => w.addSavedColor(c1, c2, gradient),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface WeatherData {
|
export interface WeatherData {
|
||||||
current: string
|
current: string
|
||||||
|
city: string
|
||||||
hourly: HourlyItem[]
|
hourly: HourlyItem[]
|
||||||
daily: DailyItem[]
|
daily: DailyItem[]
|
||||||
}
|
}
|
||||||
@@ -95,4 +96,6 @@ export interface BingState {
|
|||||||
fav: boolean
|
fav: boolean
|
||||||
idx: number
|
idx: number
|
||||||
total: number
|
total: number
|
||||||
|
hidden: boolean
|
||||||
|
market?: string
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+5
-5
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user