重构: 拆分main.go为5个文件 + 壁纸HTML移至web目录

- main.go: 入口+单实例互斥锁
- win32.go: Win32 API/embed/evalJS/全局变量
- config.go: 配置读写+图标生成
- weather.go: 天气API/定位/天气循环
- systray.go: 托盘菜单/WebView/全屏监控
- wallpaper.html → web/wallpaper.html
This commit is contained in:
2026-05-25 19:09:07 +08:00
parent 196d59269d
commit a804db3579
6 changed files with 699 additions and 697 deletions

244
systray.go Normal file
View File

@@ -0,0 +1,244 @@
package main
import (
"fmt"
"log"
"os"
"runtime"
"strconv"
"sync/atomic"
"time"
"unsafe"
"github.com/getlantern/systray"
"github.com/jchv/go-webview2"
)
var zodiacItems []*systray.MenuItem
var cityItems []*systray.MenuItem
func onSystrayReady() {
systray.SetIcon(generateIcon())
systray.SetTooltip("动态壁纸引擎")
mPause := systray.AddMenuItem("暂停", "暂停/继续")
systray.AddSeparator()
mZodiac := systray.AddMenuItem("星座设置", "")
zodiacs := []string{
"白羊座", "金牛座", "双子座",
"巨蟹座", "狮子座", "处女座",
"天秤座", "天蝎座", "射手座",
"摩羯座", "水瓶座", "双鱼座",
}
cfg := loadConfig()
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()
mQuit := systray.AddMenuItem("退出", "退出程序")
// 星座选择监听
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)
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) {
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)
for _, it := range cityItems {
it.Uncheck()
}
mi.Check()
go fetchAndPushWeather(city)
}
}(i, item)
}
// 暂停
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 {
mPause.SetTitle("暂停")
}
evalJS("if(window.setPaused) setPaused(" + strconv.FormatBool(isPaused) + ")")
}
}()
// 退出
go func() {
<-mQuit.ClickedCh
os.Exit(0)
}()
go startWebView()
go weatherLoop()
}
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")
}
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))
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))
log.Printf("配置已注入: zodiac=%s", cfg.Zodiac)
}()
go fullscreenMonitor()
// 延迟嵌入 WorkerW
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
log.Println("启动自定义消息循环...")
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
}
}
}
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)
}
}