package main import ( "fmt" "log" "os" "os/exec" "runtime" "strconv" "sync/atomic" "time" "unsafe" "github.com/getlantern/systray" "github.com/jchv/go-webview2" ) var zodiacItems []*systray.MenuItem var cityItems []*systray.MenuItem var themeItems []*systray.MenuItem var themeNames = []struct { Name ThemeName Label string }{ {ThemeAurora, "极光"}, {ThemeStar, "星空"}, {ThemeGradient, "渐变"}, {ThemeParticle, "粒子"}, {ThemeFractal, "极光流体"}, {ThemeText, "文字"}, } func onSystrayReady() { systray.SetIcon(generateIcon()) systray.SetTooltip("动态壁纸引擎") cfg := loadConfig() mPause := systray.AddMenuItem("暂停", "暂停/继续") systray.AddSeparator() // 壁纸主题 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("重启", "重启程序") 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 { <-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) + ")") } }() // 退出 go func() { <-mQuit.ClickedCh os.Exit(0) }() // 重启 go func() { for { <-mRestart.ClickedCh exe, _ := os.Executable() exec.Command(exe).Start() os.Exit(0) } }() go startWebView() go weatherLoop() go bingWallpaperLoop() } func startWebView() { runtime.LockOSThread() workerw := findWorkerW() if workerw == 0 { log.Fatal("WorkerW not found") } screenW, screenH := getScreenSize() log.Printf("Screen: %dx%d", screenW, screenH) wv = webview2.NewWithOptions(webview2.WebViewOptions{ AutoFocus: false, WindowOptions: webview2.WindowOptions{ Title: "动态壁纸", Width: uint(screenW), Height: uint(screenH), }, }) if wv == nil { log.Fatal("WebView2 create failed") } wv.Bind("setZodiacFromGo", func(zodiac string) error { cfg := loadConfig() cfg.Zodiac = zodiac return saveConfig(cfg) }) wv.SetHtml(buildWallpaperHTML(loadConfig())) time.Sleep(1 * time.Second) wvHwnd = uintptr(wv.Window()) procSetWindowLongPtrW.Call(wvHwnd, uintptr(0xFFFFFFF0), uintptr(0x80000000|0x10000000|0x02000000)) procShowWindow.Call(wvHwnd, 5) 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)) }() go fullscreenMonitor() go func() { time.Sleep(3 * time.Second) workerw := findWorkerW() if workerw != 0 { oldParent, _, _ := procSetParent.Call(wvHwnd, workerw) log.Printf("SetParent: 0x%x -> 0x%x (old=0x%x)", wvHwnd, workerw, oldParent) procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1) } }() type msg struct { hwnd uintptr message uint32 wParam uintptr lParam uintptr time uint32 pt struct{ x, y int32 } } var m msg for { ret, _, _ := procGetMessageW.Call( uintptr(unsafe.Pointer(&m)), 0, 0, 0, ) if ret == 0 { break } if m.message == wmEvalJS { for { select { case js := <-jsQueue: wv.Eval(js) default: goto nextMsg } } } 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))) } } func fullscreenMonitor() { type rect struct{ Left, Top, Right, Bottom int32 } var lastState string for { if atomic.LoadInt32(&paused) == 0 && wv != nil { fg, _, _ := procGetForegroundWindow.Call() if fg != 0 { var r rect procGetWindowRect.Call(fg, uintptr(unsafe.Pointer(&r))) screenW, screenH := getScreenSize() isFull := (r.Right-r.Left >= screenW) && (r.Bottom-r.Top >= screenH) state := strconv.FormatBool(isFull) if state != lastState { lastState = state evalJS("if(window.setFullscreen) setFullscreen(" + state + ")") } } } time.Sleep(2 * time.Second) } }