新增: 壁纸切换(主题/本地图片/Bing/纯色渐变)

This commit is contained in:
2026-05-25 19:54:32 +08:00
parent a804db3579
commit bb1574641f
14 changed files with 868 additions and 395 deletions

82
bing.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"encoding/json"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
)
const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN"
type bingResponse struct {
Images []struct {
URL string `json:"url"`
URLBase string `json:"urlbase"`
Copyright string `json:"copyright"`
} `json:"images"`
}
func fetchBingWallpaper() {
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
}
imgURL := br.Images[0].URL
if !strings.HasPrefix(imgURL, "http") {
imgURL = "https://www.bing.com" + imgURL
}
imgResp, err := httpClient.Get(imgURL)
if err != nil {
log.Println("Bing 图片下载失败:", err)
return
}
defer imgResp.Body.Close()
imgData, err := io.ReadAll(imgResp.Body)
if err != nil {
return
}
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
if err := os.WriteFile(bingPath, imgData, 0644); err != nil {
log.Println("Bing 壁纸缓存失败:", err)
return
}
log.Printf("Bing 壁纸已缓存: %s (%d bytes)", bingPath, len(imgData))
reloadWallpaper()
}
func bingWallpaperLoop() {
cfg := loadConfig()
if cfg.WallpaperType == WPBing {
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
if _, err := os.Stat(bingPath); err != nil {
fetchBingWallpaper()
}
}
ticker := time.NewTicker(4 * time.Hour)
for range ticker.C {
cfg := loadConfig()
if cfg.WallpaperType == WPBing {
fetchBingWallpaper()
}
}
}

View File

@@ -8,32 +8,80 @@ import (
"image/color"
"image/png"
"os"
"path/filepath"
)
type WallpaperType string
const (
WPTheme WallpaperType = "theme"
WPImage WallpaperType = "image"
WPBing WallpaperType = "bing"
WPColor WallpaperType = "color"
)
type ThemeName string
const (
ThemeAurora ThemeName = "aurora"
ThemeStar ThemeName = "starfield"
ThemeGradient ThemeName = "gradient"
ThemeParticle ThemeName = "particles"
)
type Config struct {
Zodiac string `json:"zodiac"`
City string `json:"city"`
WallpaperType WallpaperType `json:"wallpaperType"`
Theme ThemeName `json:"theme"`
ImagePath string `json:"imagePath"`
Color1 string `json:"color1"`
Color2 string `json:"color2"`
ColorGradient bool `json:"colorGradient"`
}
const defaultZodiac = "射手座"
var configPath string
func configDir() string {
return filepath.Dir(configPath)
}
func loadConfig() *Config {
data, err := os.ReadFile(configPath)
if err != nil {
return &Config{Zodiac: defaultZodiac}
return defaultConfig()
}
var cfg Config
if json.Unmarshal(data, &cfg) != nil {
return &Config{Zodiac: defaultZodiac}
return defaultConfig()
}
if cfg.Zodiac == "" {
cfg.Zodiac = defaultZodiac
}
if cfg.WallpaperType == "" {
cfg.WallpaperType = WPTheme
}
if cfg.Theme == "" {
cfg.Theme = ThemeAurora
}
if cfg.Color1 == "" {
cfg.Color1 = "#1a1a2e"
}
return &cfg
}
func defaultConfig() *Config {
return &Config{
Zodiac: defaultZodiac,
WallpaperType: WPTheme,
Theme: ThemeAurora,
Color1: "#1a1a2e",
Color2: "#16213e",
}
}
func saveConfig(cfg *Config) error {
data, _ := json.MarshalIndent(cfg, "", " ")
return os.WriteFile(configPath, data, 0644)

119
dialog.go Normal file
View File

@@ -0,0 +1,119 @@
package main
import (
"fmt"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
var (
comdlg32 = windows.NewLazySystemDLL("comdlg32.dll")
procGetOpenFileNameW = comdlg32.NewProc("GetOpenFileNameW")
procChooseColorW = comdlg32.NewProc("ChooseColorW")
)
func slicePtr(s interface{}) uintptr {
switch v := s.(type) {
case []uint16:
if len(v) == 0 { return 0 }
return uintptr(unsafe.Pointer(&v[0]))
case []uint32:
if len(v) == 0 { return 0 }
return uintptr(unsafe.Pointer(&v[0]))
}
return 0
}
func openFileDialog(owner uintptr) string {
type openFileName struct {
lStructSize uint32
hwndOwner uintptr
hInstance uintptr
lpstrFilter uintptr
lpstrCustomFilter uintptr
nMaxCustFilter uint32
nFilterIndex uint32
lpstrFile uintptr
nMaxFile uint32
lpstrFileTitle uintptr
nMaxFileTitle uint32
lpstrInitialDir uintptr
lpstrTitle uintptr
Flags uint32
nFileOffset uint16
nFileExtension uint16
lpstrDefExt uintptr
lCustData uintptr
lpfnHook uintptr
lpTemplateName uintptr
pvReserved uintptr
dwReserved uint32
FlagsEx uint32
}
// Filter: "Images\0*.jpg;*.png\0All\0*.*\0\0"
filterStr := "图片文件\x00*.jpg;*.jpeg;*.png;*.bmp;*.webp;*.gif\x00所有文件\x00*.*\x00\x00"
filterUTF16 := utf16.Encode([]rune(filterStr))
titleUTF16 := utf16.Encode([]rune("选择壁纸图片"))
fileBuf := make([]uint16, 260)
ofn := openFileName{
lStructSize: uint32(unsafe.Sizeof(openFileName{})),
hwndOwner: owner,
lpstrFilter: slicePtr(filterUTF16),
lpstrFile: slicePtr(fileBuf),
nMaxFile: 260,
lpstrTitle: slicePtr(titleUTF16),
Flags: 0x00001000 | 0x00000800 | 0x00000004,
}
ret, _, _ := procGetOpenFileNameW.Call(uintptr(unsafe.Pointer(&ofn)))
if ret == 0 {
return ""
}
return windows.UTF16ToString(fileBuf)
}
func colorPickerDialog(owner uintptr, initialColor string) string {
type chooseColor struct {
lStructSize uint32
hwndOwner uintptr
hInstance uintptr
rgbResult uint32
lpCustColors uintptr
Flags uint32
lCustData uintptr
lpfnHook uintptr
lpTemplateName uintptr
}
custColors := make([]uint32, 16)
var initRGB uint32
if len(initialColor) > 4 && initialColor[0] == '#' {
var r, g, b uint32
fmt.Sscanf(initialColor[1:], "%02x%02x%02x", &r, &g, &b)
initRGB = r | (g << 8) | (b << 16)
}
cc := chooseColor{
lStructSize: uint32(unsafe.Sizeof(chooseColor{})),
hwndOwner: owner,
rgbResult: initRGB,
lpCustColors: slicePtr(custColors),
Flags: 0x00000002,
}
ret, _, _ := procChooseColorW.Call(uintptr(unsafe.Pointer(&cc)))
if ret == 0 {
return ""
}
r := cc.rgbResult & 0xFF
g := (cc.rgbResult >> 8) & 0xFF
b := (cc.rgbResult >> 16) & 0xFF
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}

View File

@@ -25,9 +25,9 @@ func main() {
defer windows.CloseHandle(mutex)
exePath, _ := os.Executable()
configDir := filepath.Join(filepath.Dir(exePath), "config")
os.MkdirAll(configDir, 0755)
configPath = filepath.Join(configDir, "settings.json")
cfgDir := filepath.Join(filepath.Dir(exePath), "config")
os.MkdirAll(cfgDir, 0755)
configPath = filepath.Join(cfgDir, "settings.json")
procSetProcessDPIAware.Call()
systray.Run(onSystrayReady, nil)
}

View File

@@ -16,14 +16,44 @@ import (
var zodiacItems []*systray.MenuItem
var cityItems []*systray.MenuItem
var themeItems []*systray.MenuItem
var themeNames = []struct {
Name ThemeName
Label string
}{
{ThemeAurora, "极光"},
{ThemeStar, "星空"},
{ThemeGradient, "渐变"},
{ThemeParticle, "粒子"},
}
func onSystrayReady() {
systray.SetIcon(generateIcon())
systray.SetTooltip("动态壁纸引擎")
cfg := loadConfig()
mPause := systray.AddMenuItem("暂停", "暂停/继续")
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("本地图片", "选择本地图片作为壁纸")
mBingDaily := systray.AddMenuItem("Bing 每日壁纸", "使用 Bing 每日壁纸")
mSolidColor := systray.AddMenuItem("纯色壁纸", "选择纯色壁纸")
mGradientColor := systray.AddMenuItem("渐变壁纸", "选择渐变壁纸")
systray.AddSeparator()
// 星座
mZodiac := systray.AddMenuItem("星座设置", "")
zodiacs := []string{
"白羊座", "金牛座", "双子座",
@@ -31,7 +61,6 @@ func onSystrayReady() {
"天秤座", "天蝎座", "射手座",
"摩羯座", "水瓶座", "双鱼座",
}
cfg := loadConfig()
for _, z := range zodiacs {
item := mZodiac.AddSubMenuItem(z, z)
if z == cfg.Zodiac {
@@ -42,6 +71,7 @@ func onSystrayReady() {
systray.AddSeparator()
// 城市
mCity := systray.AddMenuItem("城市设置", "")
for _, c := range cities {
item := mCity.AddSubMenuItem(c.Name, c.Adm1+" "+c.Name)
@@ -54,14 +84,113 @@ func onSystrayReady() {
systray.AddSeparator()
mQuit := systray.AddMenuItem("退出", "退出程序")
// 主题切换监听
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 {
<-mBingDaily.ClickedCh
cfg := loadConfig()
cfg.WallpaperType = WPBing
saveConfig(cfg)
for _, it := range themeItems {
it.Uncheck()
}
log.Println("切换 Bing 壁纸")
go fetchBingWallpaper()
}
}()
// 纯色壁纸
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]
log.Printf("星座监听启动: %s", name)
for {
<-mi.ClickedCh
log.Printf("星座点击: %s", name)
cfg := loadConfig()
cfg.Zodiac = name
saveConfig(cfg)
@@ -77,11 +206,9 @@ func onSystrayReady() {
// 城市选择监听
for i, item := range cityItems {
go func(idx int, mi *systray.MenuItem) {
log.Printf("城市监听启动: %s", cities[idx].Name)
for {
<-mi.ClickedCh
city := cities[idx]
log.Printf("城市点击: %s", city.Name)
cfg := loadConfig()
cfg.City = city.ID
saveConfig(cfg)
@@ -96,13 +223,11 @@ func onSystrayReady() {
// 暂停
go func() {
log.Println("暂停监听启动")
for {
<-mPause.ClickedCh
newVal := 1 - atomic.LoadInt32(&paused)
atomic.StoreInt32(&paused, newVal)
isPaused := newVal == 1
log.Printf("暂停切换: paused=%v", isPaused)
if isPaused {
mPause.SetTitle("继续")
} else {
@@ -120,6 +245,7 @@ func onSystrayReady() {
go startWebView()
go weatherLoop()
go bingWallpaperLoop()
}
func startWebView() {
@@ -145,18 +271,13 @@ func startWebView() {
log.Fatal("WebView2 create failed")
}
htmlData, err := fs.ReadFile("web/wallpaper.html")
if err != nil {
log.Fatal("读取 wallpaper.html 失败:", err)
}
wv.Bind("setZodiacFromGo", func(zodiac string) error {
cfg := loadConfig()
cfg.Zodiac = zodiac
return saveConfig(cfg)
})
wv.SetHtml(string(htmlData))
wv.SetHtml(buildWallpaperHTML(loadConfig()))
time.Sleep(1 * time.Second)
wvHwnd = uintptr(wv.Window())
@@ -165,17 +286,14 @@ func startWebView() {
procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
log.Printf("壁纸已嵌入: HWND=0x%x, %dx%d", wvHwnd, screenW, screenH)
// 注入配置
go func() {
time.Sleep(500 * time.Millisecond)
cfg := loadConfig()
evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
log.Printf("配置已注入: zodiac=%s", cfg.Zodiac)
}()
go fullscreenMonitor()
// 延迟嵌入 WorkerW
go func() {
time.Sleep(3 * time.Second)
workerw := findWorkerW()
@@ -186,7 +304,6 @@ func startWebView() {
}
}()
// 消息循环
type msg struct {
hwnd uintptr
message uint32
@@ -196,7 +313,6 @@ func startWebView() {
pt struct{ x, y int32 }
}
var m msg
log.Println("启动自定义消息循环...")
for {
ret, _, _ := procGetMessageW.Call(
uintptr(unsafe.Pointer(&m)),
@@ -215,6 +331,14 @@ func startWebView() {
}
}
}
if m.message == wmSetHtml {
select {
case html := <-htmlQueue:
wv.SetHtml(html)
default:
}
goto nextMsg
}
nextMsg:
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))

113
wallpaper.go Normal file
View File

@@ -0,0 +1,113 @@
package main
import (
_ "embed"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
)
//go:embed web/overlay.html
var overlayHTML string
//go:embed web/themes/aurora.html
var themeAurora string
//go:embed web/themes/starfield.html
var themeStarfield string
//go:embed web/themes/gradient.html
var themeGradient string
//go:embed web/themes/particles.html
var themeParticles string
var themeMap = map[ThemeName]string{
ThemeAurora: themeAurora,
ThemeStar: themeStarfield,
ThemeGradient: themeGradient,
ThemeParticle: themeParticles,
}
func buildWallpaperHTML(cfg *Config) string {
var bg string
switch cfg.WallpaperType {
case WPTheme:
if t, ok := themeMap[cfg.Theme]; ok {
bg = t
} else {
bg = themeAurora
}
case WPImage:
if cfg.ImagePath != "" {
src := imageToDataURI(cfg.ImagePath)
if src != "" {
bg = fmt.Sprintf(`<img src="%s" style="position:fixed;top:0;left:0;width:100%%;height:100%%;object-fit:cover;z-index:1;">`, src)
}
}
case WPBing:
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
if _, err := os.Stat(bingPath); err == nil {
src := imageToDataURI(bingPath)
if src != "" {
bg = fmt.Sprintf(`<img src="%s" style="position:fixed;top:0;left:0;width:100%%;height:100%%;object-fit:cover;z-index:1;">`, src)
}
}
case WPColor:
if cfg.ColorGradient && cfg.Color2 != "" {
bg = fmt.Sprintf(`<div style="position:fixed;top:0;left:0;width:100%%;height:100%%;z-index:1;background:linear-gradient(135deg,%s,%s);"></div>`, cfg.Color1, cfg.Color2)
} else {
bg = fmt.Sprintf(`<div style="position:fixed;top:0;left:0;width:100%%;height:100%%;z-index:1;background:%s;"></div>`, cfg.Color1)
}
}
if bg == "" {
bg = themeAurora
}
return strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1)
}
func imageToDataURI(path string) string {
data, err := os.ReadFile(path)
if err != nil {
log.Println("读取图片失败:", err)
return ""
}
ext := strings.ToLower(filepath.Ext(path))
mime := "image/jpeg"
switch ext {
case ".png":
mime = "image/png"
case ".gif":
mime = "image/gif"
case ".webp":
mime = "image/webp"
case ".bmp":
mime = "image/bmp"
}
return fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(data))
}
func reloadWallpaper() {
if wv == nil || wvHwnd == 0 {
return
}
cfg := loadConfig()
html := buildWallpaperHTML(cfg)
select {
case htmlQueue <- html:
default:
}
procPostMessageW.Call(wvHwnd, wmSetHtml, 0, 0)
go func() {
time.Sleep(1 * time.Second)
evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
city := getCurrentCity()
go fetchAndPushWeather(city)
}()
}

View File

@@ -70,7 +70,7 @@ func getLocation() City {
if city := locateByIPIP(); city != nil {
return *city
}
if city := locateByQWeather(); city != nil {
if city := locateByGeoAPI(); city != nil {
return *city
}
log.Println("所有定位失败,使用默认城市:", defaultCity.Name)
@@ -104,7 +104,7 @@ func locateByIPIP() *City {
return nil
}
func locateByQWeather() *City {
func locateByGeoAPI() *City {
data, err := httpGet("https://myip.ipip.net")
if err == nil {
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)

133
web/overlay.html Normal file
View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
#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;
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);
text-align: right;
min-width: 300px;
max-height: 90vh;
overflow-y: auto;
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; }
</style>
</head>
<body>
{{BACKGROUND}}
<div id="info">
<div class="time" id="time">00:00</div>
<div class="date" id="date">1月1日 周一</div>
<div class="weather-section">
<div class="current-weather" id="currentWeather">🌤️ 加载中...</div>
<div class="forecast-title">未来24小时</div>
<div class="weather-forecast" id="hourlyForecast"></div>
<div class="forecast-title" style="margin-top: 12px;">未来7天</div>
<div class="daily-forecast" id="dailyForecast"></div>
</div>
<div class="zodiac" id="zodiac">✨ 射手座运势</div>
</div>
<script>
let lastTimeStr = '';
let lastDateStr = '';
let lastZodiac = '';
window.updateWeatherFromGo = function(data) {
if (typeof data === 'string') data = JSON.parse(data);
var el = document.getElementById('currentWeather');
if (el && data.current) el.textContent = data.current;
var fel = document.getElementById('hourlyForecast');
if (fel) {
if (data.hourly && data.hourly.length > 0) {
fel.innerHTML = data.hourly.map(function(item) {
return '<div class="forecast-item"><div class="forecast-time">' + item.time + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.temp + '</div></div>';
}).join('');
} else { fel.innerHTML = '<div style="font-size:11px;opacity:0.5">暂无数据</div>'; }
}
var del = document.getElementById('dailyForecast');
if (del) {
if (data.daily && data.daily.length > 0) {
del.innerHTML = data.daily.map(function(item) {
return '<div class="daily-item"><div style="opacity:0.8;margin-bottom:3px">' + item.date + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.tempMin + '°~' + item.tempMax + '°</div></div>';
}).join('');
} else { del.innerHTML = '<div style="font-size:11px;opacity:0.5">暂无数据</div>'; }
}
};
var fortunes = {
'白羊座':'今日运势旺盛,适合开展新计划。','金牛座':'财运不错,但需注意健康。',
'双子座':'人际关系活跃,社交运势佳。','巨蟹座':'情绪敏感,适合独处思考。',
'狮子座':'自信爆棚,工作表现突出。','处女座':'细节决定成败,专注当下。',
'天秤座':'感情运佳,单身者有机会。','天蝎座':'直觉敏锐,适合做决策。',
'射手座':'冒险精神旺盛,出行注意安全。','摩羯座':'事业运佳,工作效率高。',
'水瓶座':'创新思维活跃,灵感不断。','双鱼座':'艺术灵感丰富,适合创作。'
};
function getUserZodiac() { return window.userZodiac || '射手座'; }
function updateZodiacDisplay() {
var name = getUserZodiac();
if (name === lastZodiac) return;
lastZodiac = name;
var el = document.getElementById('zodiac');
if (!el) return;
var fortune = fortunes[name] || '运势平稳,保持平常心。';
el.innerHTML = '✨ ' + name + '运势<br><small style="opacity:0.7">' + fortune + '</small>';
}
function updateTime() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var month = now.getMonth() + 1;
var day = now.getDate();
var week = ['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
var timeStr = hh + ':' + mm;
var dateStr = month + '月' + day + '日 ' + week;
var timeEl = document.getElementById('time');
var dateEl = document.getElementById('date');
if (timeEl && timeStr !== lastTimeStr) { timeEl.textContent = timeStr; lastTimeStr = timeStr; }
if (dateEl && dateStr !== lastDateStr) { dateEl.textContent = dateStr; lastDateStr = dateStr; }
updateZodiacDisplay();
}
updateTime();
setInterval(updateTime, 1000);
</script>
</body>
</html>

77
web/themes/aurora.html Normal file
View File

@@ -0,0 +1,77 @@
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
<script>
var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
var vsSource = 'attribute vec4 aVertexPosition;void main(){gl_Position=aVertexPosition;}';
var fsSource = `
precision mediump float;
uniform float uTime;
uniform vec2 uResolution;
void main() {
vec2 uv=gl_FragCoord.xy/uResolution.xy;
vec3 c1=vec3(0.1,0.4,0.8);
vec3 c2=vec3(0.4,0.2,0.8);
vec3 c3=vec3(0.1,0.6,0.4);
float w1=sin(uv.x*3.0+uTime*0.5)*0.5+0.5;
float w2=sin(uv.x*4.0+uTime*0.3+uv.y*2.0)*0.5+0.5;
float w3=sin(uv.x*2.0+uTime*0.7+uv.y*3.0)*0.5+0.5;
float d=uv.y*2.0-1.0;
float g=exp(-d*d*2.0);
vec3 a=(c1*w1+c2*w2+c3*w3)*g*0.6;
vec3 b=mix(vec3(0.02,0.02,0.08),vec3(0.05,0.05,0.15),uv.y);
gl_FragColor=vec4(b+a,1.0);
}
`;
function loadShader(gl,type,source){
var s=gl.createShader(type);
gl.shaderSource(s,source);
gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){gl.deleteShader(s);return null;}
return s;
}
var vs=loadShader(gl,gl.VERTEX_SHADER,vsSource);
var fs=loadShader(gl,gl.FRAGMENT_SHADER,fsSource);
var prog=gl.createProgram();
gl.attachShader(prog,vs);
gl.attachShader(prog,fs);
gl.linkProgram(prog);
var posLoc=gl.getAttribLocation(prog,'aVertexPosition');
var timeLoc=gl.getUniformLocation(prog,'uTime');
var resLoc=gl.getUniformLocation(prog,'uResolution');
var posBuf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,1,1,1,-1,-1,1,-1]),gl.STATIC_DRAW);
var startTime=Date.now();
var FPS=15,lastFrame=0;
function render(ts){
requestAnimationFrame(render);
if(ts-lastFrame<1000/FPS)return;
lastFrame=ts;
gl.viewport(0,0,canvas.width,canvas.height);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.vertexAttribPointer(posLoc,2,gl.FLOAT,false,0,0);
gl.uniform1f(timeLoc,(Date.now()-startTime)/1000);
gl.uniform2f(resLoc,canvas.width,canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
requestAnimationFrame(render);
</script>

17
web/themes/gradient.html Normal file
View File

@@ -0,0 +1,17 @@
<style>
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
#gradient-bg {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
background: linear-gradient(-45deg, #0f0c29, #302b63, #24243e, #1a1a2e, #16213e);
background-size: 400% 400%;
animation: gradientShift 20s ease infinite;
}
</style>
<div id="gradient-bg"></div>

60
web/themes/particles.html Normal file
View File

@@ -0,0 +1,60 @@
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
var particles = [];
for (var i = 0; i < 80; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 3 + 1,
vx: (Math.random() - 0.5) * 0.5,
vy: -(Math.random() * 0.8 + 0.2),
alpha: Math.random() * 0.5 + 0.2,
hue: Math.random() * 60 + 200
});
}
var FPS = 15, lastFrame = 0;
function render(ts) {
requestAnimationFrame(render);
if (ts - lastFrame < 1000 / FPS) return;
lastFrame = ts;
var w = canvas.width, h = canvas.height;
ctx.fillStyle = 'rgba(5, 5, 15, 0.15)';
ctx.fillRect(0, 0, w, h);
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
if (p.y < -20) { p.y = h + 20; p.x = Math.random() * w; }
if (p.x < -20) p.x = w + 20;
if (p.x > w + 20) p.x = -20;
var grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 4);
grad.addColorStop(0, 'hsla(' + p.hue + ', 70%, 70%, ' + p.alpha + ')');
grad.addColorStop(1, 'hsla(' + p.hue + ', 70%, 70%, 0)');
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 4, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + p.hue + ', 80%, 80%, ' + (p.alpha + 0.3) + ')';
ctx.fill();
}
}
requestAnimationFrame(render);
</script>

64
web/themes/starfield.html Normal file
View File

@@ -0,0 +1,64 @@
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
var stars = [];
for (var i = 0; i < 300; i++) {
stars.push({
x: (Math.random() - 0.5) * canvas.width,
y: (Math.random() - 0.5) * canvas.height,
z: Math.random() * canvas.width,
pz: 0
});
}
var FPS = 15, lastFrame = 0;
function render(ts) {
requestAnimationFrame(render);
if (ts - lastFrame < 1000 / FPS) return;
lastFrame = ts;
var w = canvas.width, h = canvas.height;
var cx = w / 2, cy = h / 2;
ctx.fillStyle = 'rgba(2, 3, 12, 0.25)';
ctx.fillRect(0, 0, w, h);
var speed = 8;
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
s.z -= speed;
if (s.z <= 0) {
s.x = (Math.random() - 0.5) * w;
s.y = (Math.random() - 0.5) * h;
s.z = w;
s.pz = s.z;
}
var sx = (s.x / s.z) * w * 0.5 + cx;
var sy = (s.y / s.z) * h * 0.5 + cy;
var px = (s.x / s.pz) * w * 0.5 + cx;
var py = (s.y / s.pz) * h * 0.5 + cy;
s.pz = s.z;
var r = Math.max(0.5, (1 - s.z / w) * 2.5);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.strokeStyle = 'rgba(200, 220, 255, ' + ((1 - s.z / w) * 0.8) + ')';
ctx.lineWidth = r;
ctx.stroke();
ctx.beginPath();
ctx.arc(sx, sy, r * 0.6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(220, 235, 255, ' + ((1 - s.z / w) * 0.9) + ')';
ctx.fill();
}
}
requestAnimationFrame(render);
</script>

View File

@@ -1,356 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>动态壁纸</title>
<style>
* { margin: 0; padding: 0; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
#canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#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;
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);
text-align: right;
min-width: 300px;
max-height: 90vh;
overflow-y: auto;
z-index: 10;
}
.time {
font-size: 64px;
font-weight: 300;
color: #ffffff;
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;
font-weight: 400;
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);
font-weight: 400;
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;
cursor: default;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
.current-weather {
font-size: 17px;
color: rgba(255, 255, 255, 0.95);
font-weight: 400;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
margin-bottom: 12px;
}
.weather-forecast::-webkit-scrollbar,
#info::-webkit-scrollbar {
display: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="info">
<div class="time" id="time">00:00</div>
<div class="date" id="date">1月1日 周一</div>
<div class="weather-section">
<div class="current-weather" id="currentWeather">🌤️ 加载中...</div>
<div class="forecast-title">未来24小时</div>
<div class="weather-forecast" id="hourlyForecast"></div>
<div class="forecast-title" style="margin-top: 12px;">未来7天</div>
<div class="daily-forecast" id="dailyForecast"></div>
</div>
<div class="zodiac" id="zodiac">✨ 射手座运势</div>
</div>
<script>
console.log('=== 页面已加载 ===');
let lastTimeStr = '';
let lastDateStr = '';
let lastZodiac = '';
// 天气渲染:由 Go 层推送数据
window.updateWeatherFromGo = function(data) {
if (typeof data === 'string') data = JSON.parse(data);
const currentEl = document.getElementById('currentWeather');
if (currentEl && data.current) currentEl.textContent = data.current;
const forecastEl = document.getElementById('hourlyForecast');
if (forecastEl) {
if (data.hourly && data.hourly.length > 0) {
forecastEl.innerHTML = data.hourly.map(item =>
'<div class="forecast-item"><div class="forecast-time">' + item.time + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.temp + '</div></div>'
).join('');
} else {
forecastEl.innerHTML = '<div style="font-size:11px; opacity:0.5;">暂无数据</div>';
}
}
const dailyEl = document.getElementById('dailyForecast');
if (dailyEl) {
if (data.daily && data.daily.length > 0) {
dailyEl.innerHTML = data.daily.map(item =>
'<div class="daily-item"><div style="opacity:0.8;margin-bottom:3px">' + item.date + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.tempMin + '°~' + item.tempMax + '°</div></div>'
).join('');
} else {
dailyEl.innerHTML = '<div style="font-size:11px; opacity:0.5;">暂无数据</div>';
}
}
console.log('✅ 天气已更新:', data.current);
};
// 星座运势
const fortunes = {
'白羊座': '今日运势旺盛,适合开展新计划。',
'金牛座': '财运不错,但需注意健康。',
'双子座': '人际关系活跃,社交运势佳。',
'巨蟹座': '情绪敏感,适合独处思考。',
'狮子座': '自信爆棚,工作表现突出。',
'处女座': '细节决定成败,专注当下。',
'天秤座': '感情运佳,单身者有机会。',
'天蝎座': '直觉敏锐,适合做决策。',
'射手座': '冒险精神旺盛,出行注意安全。',
'摩羯座': '事业运佳,工作效率高。',
'水瓶座': '创新思维活跃,灵感不断。',
'双鱼座': '艺术灵感丰富,适合创作。'
};
// WebGL 极光背景
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
const vsSource = 'attribute vec4 aVertexPosition;void main(){gl_Position=aVertexPosition;}';
const fsSource = `
precision mediump float;
uniform float uTime;
uniform vec2 uResolution;
void main() {
vec2 uv=gl_FragCoord.xy/uResolution.xy;
vec3 c1=vec3(0.1,0.4,0.8);
vec3 c2=vec3(0.4,0.2,0.8);
vec3 c3=vec3(0.1,0.6,0.4);
float w1=sin(uv.x*3.0+uTime*0.5)*0.5+0.5;
float w2=sin(uv.x*4.0+uTime*0.3+uv.y*2.0)*0.5+0.5;
float w3=sin(uv.x*2.0+uTime*0.7+uv.y*3.0)*0.5+0.5;
float d=uv.y*2.0-1.0;
float g=exp(-d*d*2.0);
vec3 a=(c1*w1+c2*w2+c3*w3)*g*0.6;
vec3 b=mix(vec3(0.02,0.02,0.08),vec3(0.05,0.05,0.15),uv.y);
gl_FragColor=vec4(b+a,1.0);
}
`;
function loadShader(gl,type,source){
const s=gl.createShader(type);
gl.shaderSource(s,source);
gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){
console.error('Shader error:',gl.getShaderInfoLog(s));
gl.deleteShader(s);
return null;
}
return s;
}
const vs=loadShader(gl,gl.VERTEX_SHADER,vsSource);
const fs=loadShader(gl,gl.FRAGMENT_SHADER,fsSource);
const prog=gl.createProgram();
gl.attachShader(prog,vs);
gl.attachShader(prog,fs);
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){
console.error('Program link error:',gl.getProgramInfoLog(prog));
}
const posLoc=gl.getAttribLocation(prog,'aVertexPosition');
const timeLoc=gl.getUniformLocation(prog,'uTime');
const resLoc=gl.getUniformLocation(prog,'uResolution');
const posBuf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,1,1,1,-1,-1,1,-1]),gl.STATIC_DRAW);
let startTime=Date.now();
const FPS = 15;
let lastFrame = 0;
function render(ts){
requestAnimationFrame(render);
if (ts - lastFrame < 1000 / FPS) return;
lastFrame = ts;
gl.viewport(0,0,canvas.width,canvas.height);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.vertexAttribPointer(posLoc,2,gl.FLOAT,false,0,0);
gl.uniform1f(timeLoc,(Date.now()-startTime)/1000);
gl.uniform2f(resLoc,canvas.width,canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
requestAnimationFrame(render);
console.log('✅ WebGL 极光背景已启动');
// 时间和星座
function getUserZodiac() {
return window.userZodiac || '射手座';
}
function updateZodiacDisplay() {
var name = getUserZodiac();
if (name === lastZodiac) return;
lastZodiac = name;
var zodiacEl = document.getElementById('zodiac');
if (!zodiacEl) return;
var fortune = fortunes[name] || '运势平稳,保持平常心。';
zodiacEl.innerHTML = '✨ ' + name + '运势<br><small style="opacity:0.7">' + fortune + '</small>';
}
function updateTime() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var month = now.getMonth() + 1;
var day = now.getDate();
var week = ['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
var timeEl = document.getElementById('time');
var dateEl = document.getElementById('date');
var timeStr = hh + ':' + mm;
var dateStr = month + '月' + day + '日 ' + week;
if (timeEl && timeStr !== lastTimeStr) {
timeEl.textContent = timeStr;
lastTimeStr = timeStr;
}
if (dateEl && dateStr !== lastDateStr) {
dateEl.textContent = dateStr;
lastDateStr = dateStr;
}
updateZodiacDisplay();
}
updateTime();
setInterval(updateTime, 1000);
console.log('=== 初始化完成 ===');
</script>
</body>
</html>

View File

@@ -1,7 +1,6 @@
package main
import (
"embed"
"log"
"unsafe"
@@ -9,9 +8,6 @@ import (
"golang.org/x/sys/windows"
)
//go:embed web/wallpaper.html
var fs embed.FS
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procFindWindowW = user32.NewProc("FindWindowW")
@@ -37,6 +33,9 @@ var jsQueue = make(chan string, 64)
var paused int32
const wmEvalJS = 0x0401
const wmSetHtml = 0x0402
var htmlQueue = make(chan string, 1)
func evalJS(js string) {
select {
@@ -50,13 +49,6 @@ func evalJS(js string) {
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func findWorkerW() uintptr {
progman, _, _ := procFindWindowW.Call(
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)