重构: 拆分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:
64
config.go
Normal file
64
config.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Zodiac string `json:"zodiac"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
const defaultZodiac = "射手座"
|
||||
|
||||
var configPath string
|
||||
|
||||
func loadConfig() *Config {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
}
|
||||
var cfg Config
|
||||
if json.Unmarshal(data, &cfg) != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
}
|
||||
if cfg.Zodiac == "" {
|
||||
cfg.Zodiac = defaultZodiac
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func saveConfig(cfg *Config) error {
|
||||
data, _ := json.MarshalIndent(cfg, "", " ")
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
func generateIcon() []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 16, 16))
|
||||
c := color.RGBA{R: 88, G: 101, B: 242, A: 255}
|
||||
for y := 0; y < 16; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
png.Encode(&buf, img)
|
||||
pngData := buf.Bytes()
|
||||
ico := make([]byte, 22+len(pngData))
|
||||
binary.LittleEndian.PutUint16(ico[0:], 0)
|
||||
binary.LittleEndian.PutUint16(ico[2:], 1)
|
||||
binary.LittleEndian.PutUint16(ico[4:], 1)
|
||||
ico[6], ico[7], ico[8], ico[9] = 16, 16, 0, 0
|
||||
binary.LittleEndian.PutUint16(ico[10:], 1)
|
||||
binary.LittleEndian.PutUint16(ico[12:], 32)
|
||||
binary.LittleEndian.PutUint32(ico[14:], uint32(len(pngData)))
|
||||
binary.LittleEndian.PutUint32(ico[18:], 22)
|
||||
copy(ico[22:], pngData)
|
||||
return ico
|
||||
}
|
||||
698
main.go
698
main.go
@@ -1,105 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/jchv/go-webview2"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//go:embed wallpaper.html
|
||||
var fs embed.FS
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
procFindWindowW = user32.NewProc("FindWindowW")
|
||||
procFindWindowExW = user32.NewProc("FindWindowExW")
|
||||
procSendMessageTimeoutW = user32.NewProc("SendMessageTimeoutW")
|
||||
procSetParent = user32.NewProc("SetParent")
|
||||
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
procMoveWindow = user32.NewProc("MoveWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procPostMessageW = user32.NewProc("PostMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||
)
|
||||
|
||||
var wv webview2.WebView
|
||||
var configPath string
|
||||
var wvHwnd uintptr
|
||||
var jsQueue = make(chan string, 64)
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
var paused int32 // atomic: 0=running, 1=paused
|
||||
|
||||
const wmEvalJS = 0x0401 // WM_USER + 1
|
||||
|
||||
func evalJS(js string) {
|
||||
select {
|
||||
case jsQueue <- js:
|
||||
log.Println("📤 evalJS queued:", js[:min(60, len(js))])
|
||||
default:
|
||||
log.Println("⚠️ jsQueue full, dropping eval")
|
||||
}
|
||||
if wvHwnd != 0 {
|
||||
ret, _, _ := procPostMessageW.Call(wvHwnd, wmEvalJS, 0, 0)
|
||||
log.Printf("📤 PostMessageW ret=%d hwnd=0x%x", ret, wvHwnd)
|
||||
} else {
|
||||
log.Println("⚠️ wvHwnd is 0, skip PostMessage")
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ── Config ──
|
||||
|
||||
type Config struct {
|
||||
Zodiac string `json:"zodiac"`
|
||||
City string `json:"city"` // QWeather city ID, 空=自动定位
|
||||
}
|
||||
|
||||
const defaultZodiac = "射手座"
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
// 单实例互斥锁,防止双托盘
|
||||
mutexName, _ := windows.UTF16PtrFromString("Global\\u-desktop-single-instance")
|
||||
mutex, err := windows.CreateMutex(nil, false, mutexName)
|
||||
if err != nil {
|
||||
log.Fatal("创建互斥锁失败:", err)
|
||||
}
|
||||
if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS {
|
||||
log.Println("⚠️ 已有实例运行,退出")
|
||||
log.Println("已有实例运行,退出")
|
||||
windows.CloseHandle(mutex)
|
||||
return
|
||||
}
|
||||
@@ -112,618 +31,3 @@ func main() {
|
||||
procSetProcessDPIAware.Call()
|
||||
systray.Run(onSystrayReady, nil)
|
||||
}
|
||||
|
||||
func loadConfig() *Config {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
}
|
||||
var cfg Config
|
||||
if json.Unmarshal(data, &cfg) != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
}
|
||||
if cfg.Zodiac == "" {
|
||||
cfg.Zodiac = defaultZodiac
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func saveConfig(cfg *Config) error {
|
||||
data, _ := json.MarshalIndent(cfg, "", " ")
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// ── Icon ──
|
||||
|
||||
func generateIcon() []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 16, 16))
|
||||
c := color.RGBA{R: 88, G: 101, B: 242, A: 255}
|
||||
for y := 0; y < 16; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
png.Encode(&buf, img)
|
||||
pngData := buf.Bytes()
|
||||
ico := make([]byte, 22+len(pngData))
|
||||
binary.LittleEndian.PutUint16(ico[0:], 0)
|
||||
binary.LittleEndian.PutUint16(ico[2:], 1)
|
||||
binary.LittleEndian.PutUint16(ico[4:], 1)
|
||||
ico[6], ico[7], ico[8], ico[9] = 16, 16, 0, 0
|
||||
binary.LittleEndian.PutUint16(ico[10:], 1)
|
||||
binary.LittleEndian.PutUint16(ico[12:], 32)
|
||||
binary.LittleEndian.PutUint32(ico[14:], uint32(len(pngData)))
|
||||
binary.LittleEndian.PutUint32(ico[18:], 22)
|
||||
copy(ico[22:], pngData)
|
||||
return ico
|
||||
}
|
||||
|
||||
// ── Weather ──
|
||||
|
||||
const qweatherKey = "3b67b65a53c04170b602d2d1a7e6096f"
|
||||
const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com"
|
||||
|
||||
type City struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Adm2 string `json:"adm2"`
|
||||
Adm1 string `json:"adm1"`
|
||||
}
|
||||
|
||||
var cities = []City{
|
||||
{"101010100", "北京", "北京", "北京"},
|
||||
{"101020100", "上海", "上海", "上海"},
|
||||
{"101280101", "广州", "广州", "广东"},
|
||||
{"101280601", "深圳", "深圳", "广东"},
|
||||
{"101200101", "武汉", "武汉", "湖北"},
|
||||
{"101110101", "西安", "西安", "陕西"},
|
||||
{"101210101", "杭州", "杭州", "浙江"},
|
||||
{"101190101", "南京", "南京", "江苏"},
|
||||
{"101070101", "沈阳", "沈阳", "辽宁"},
|
||||
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
|
||||
{"101250101", "长沙", "长沙", "湖南"},
|
||||
{"101270101", "成都", "成都", "四川"},
|
||||
}
|
||||
|
||||
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
|
||||
|
||||
type hourlyItem struct {
|
||||
Time string `json:"time"`
|
||||
Temp string `json:"temp"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type dailyItem struct {
|
||||
Date string `json:"date"`
|
||||
Icon string `json:"icon"`
|
||||
TempMin string `json:"tempMin"`
|
||||
TempMax string `json:"tempMax"`
|
||||
}
|
||||
|
||||
var weatherIcons = map[string]string{
|
||||
"晴": "☀️", "多云": "⛅", "阴": "☁️",
|
||||
"雨": "🌧️", "雪": "❄️", "雷": "⛈️",
|
||||
"雾": "🌫️", "霾": "😷", "风": "💨",
|
||||
}
|
||||
|
||||
func getWeatherIcon(text string) string {
|
||||
for k, v := range weatherIcons {
|
||||
if strings.Contains(text, k) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "🌤️"
|
||||
}
|
||||
|
||||
func httpGet(url string) ([]byte, error) {
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// getLocation 自动定位,返回城市
|
||||
func getLocation() City {
|
||||
// 1. 尝试 ipip.net
|
||||
if city := locateByIPIP(); city != nil {
|
||||
return *city
|
||||
}
|
||||
// 2. 尝试 QWeather GeoAPI
|
||||
if city := locateByQWeather(); city != nil {
|
||||
return *city
|
||||
}
|
||||
// 3. fallback
|
||||
log.Println("⚠️ 所有定位失败,使用默认城市:", defaultCity.Name)
|
||||
return defaultCity
|
||||
}
|
||||
|
||||
func locateByIPIP() *City {
|
||||
data, err := httpGet("https://myip.ipip.net")
|
||||
if err != nil {
|
||||
log.Println("ipip.net 不可用:", err)
|
||||
return nil
|
||||
}
|
||||
text := string(data)
|
||||
log.Println("ipip.net 响应:", text)
|
||||
re := regexp.MustCompile(`来自于[::]\s*(.+?)\s*$`)
|
||||
m := re.FindStringSubmatch(text)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Fields(m[1])
|
||||
cityName := parts[0]
|
||||
if len(parts) >= 3 {
|
||||
cityName = parts[2]
|
||||
} else if len(parts) >= 2 {
|
||||
cityName = parts[1]
|
||||
}
|
||||
for _, c := range cities {
|
||||
if c.Name == cityName {
|
||||
log.Println("✅ ipip.net 匹配:", c.Name)
|
||||
return &c
|
||||
}
|
||||
}
|
||||
log.Println("ipip.net 城市", cityName, "不在预置列表中")
|
||||
return nil
|
||||
}
|
||||
|
||||
func locateByQWeather() *City {
|
||||
data, err := httpGet("https://myip.ipip.net")
|
||||
if err == nil {
|
||||
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
|
||||
if m := re.FindStringSubmatch(string(data)); m != nil {
|
||||
return geoLookup(m[1])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func geoLookup(ip string) *City {
|
||||
url := fmt.Sprintf(qweatherHost + "/v2/city/lookup?location=%s&key=%s", ip, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Location []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Adm2 string `json:"adm2"`
|
||||
Adm1 string `json:"adm1"`
|
||||
} `json:"location"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 {
|
||||
return nil
|
||||
}
|
||||
loc := resp.Location[0]
|
||||
// 优先匹配预置列表
|
||||
for _, c := range cities {
|
||||
if c.Name == loc.Name {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1}
|
||||
}
|
||||
|
||||
// getCurrentCity 获取当前城市(手动优先 > 自动定位)
|
||||
func getCurrentCity() City {
|
||||
cfg := loadConfig()
|
||||
if cfg.City != "" {
|
||||
for _, c := range cities {
|
||||
if c.ID == cfg.City {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return getLocation()
|
||||
}
|
||||
|
||||
// fetchAndPushWeather 获取天气并推送给 JS
|
||||
func fetchAndPushWeather(city City) {
|
||||
type weatherData struct {
|
||||
Current string `json:"current"`
|
||||
Hourly []hourlyItem `json:"hourly"`
|
||||
Daily []dailyItem `json:"daily"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
wd := weatherData{}
|
||||
cityName := city.Name
|
||||
if city.Adm2 != city.Name {
|
||||
cityName = city.Adm2 + " " + city.Name
|
||||
}
|
||||
wd.City = cityName
|
||||
|
||||
// 当前天气
|
||||
if now := fetchCurrentWeather(city.ID); now != nil {
|
||||
icon := getWeatherIcon(now.Text)
|
||||
wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp)
|
||||
} else {
|
||||
wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName)
|
||||
}
|
||||
|
||||
// 24h 预报
|
||||
wd.Hourly = fetchHourlyForecast(city.ID)
|
||||
|
||||
// 7d 预报
|
||||
wd.Daily = fetchDailyForecast(city.ID)
|
||||
|
||||
jsonData, _ := json.Marshal(wd)
|
||||
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
|
||||
log.Println("✅ 天气已推送:", wd.Current)
|
||||
}
|
||||
|
||||
type currentWeather struct {
|
||||
Text string `json:"text"`
|
||||
Temp string `json:"temp"`
|
||||
}
|
||||
|
||||
func fetchCurrentWeather(cityID string) *currentWeather {
|
||||
url := fmt.Sprintf(qweatherHost + "/v7/weather/now?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Now struct {
|
||||
Text string `json:"text"`
|
||||
Temp string `json:"temp"`
|
||||
} `json:"now"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
return ¤tWeather{resp.Now.Text, resp.Now.Temp}
|
||||
}
|
||||
|
||||
func fetchHourlyForecast(cityID string) []hourlyItem {
|
||||
url := fmt.Sprintf(qweatherHost + "/v7/weather/24h?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Hourly []struct {
|
||||
FxTime string `json:"fxTime"`
|
||||
Temp string `json:"temp"`
|
||||
Text string `json:"text"`
|
||||
} `json:"hourly"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
var result []hourlyItem
|
||||
for i, h := range resp.Hourly {
|
||||
if i >= 8 {
|
||||
break
|
||||
}
|
||||
// fxTime: "2026-05-25T14:00+08:00" -> "14:00"
|
||||
t := h.FxTime
|
||||
if idx := strings.Index(t, "T"); idx >= 0 {
|
||||
t = t[idx+1:]
|
||||
if len(t) >= 5 {
|
||||
t = t[:5]
|
||||
}
|
||||
}
|
||||
result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text)})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchDailyForecast(cityID string) []dailyItem {
|
||||
url := fmt.Sprintf(qweatherHost + "/v7/weather/7d?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Daily []struct {
|
||||
FxDate string `json:"fxDate"`
|
||||
TextDay string `json:"textDay"`
|
||||
TempMin string `json:"tempMin"`
|
||||
TempMax string `json:"tempMax"`
|
||||
} `json:"daily"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var result []dailyItem
|
||||
for _, d := range resp.Daily {
|
||||
date := d.FxDate
|
||||
if date == today {
|
||||
date = "今天"
|
||||
} else {
|
||||
if t, err := time.Parse("2006-01-02", date); err == nil {
|
||||
date = weekdays[t.Weekday()]
|
||||
}
|
||||
}
|
||||
result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func weatherLoop() {
|
||||
// 首次延迟等 WebView 初始化
|
||||
time.Sleep(3 * time.Second)
|
||||
city := getCurrentCity()
|
||||
log.Println("🌤️ 初始城市:", city.Name)
|
||||
fetchAndPushWeather(city)
|
||||
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
for range ticker.C {
|
||||
city := getCurrentCity()
|
||||
fetchAndPushWeather(city)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Systray ──
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// ── WebView ──
|
||||
|
||||
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("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 {
|
||||
log.Println("收到 wmEvalJS, drain queue...")
|
||||
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 findWorkerW() uintptr {
|
||||
progman, _, _ := procFindWindowW.Call(
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
if progman == 0 {
|
||||
return 0
|
||||
}
|
||||
var result uintptr
|
||||
procSendMessageTimeoutW.Call(progman, 0x052C, 0, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&result)))
|
||||
shellDefView, _, _ := procFindWindowExW.Call(progman, 0,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView"))), 0)
|
||||
workerwAfterShell, _, _ := procFindWindowExW.Call(progman, shellDefView,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
if workerwAfterShell != 0 {
|
||||
return workerwAfterShell
|
||||
}
|
||||
ww, _, _ := procFindWindowExW.Call(progman, 0,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
return ww
|
||||
}
|
||||
|
||||
func getScreenSize() (int32, int32) {
|
||||
w, _, _ := procGetSystemMetrics.Call(0)
|
||||
h, _, _ := procGetSystemMetrics.Call(1)
|
||||
return int32(w), int32(h)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
244
systray.go
Normal file
244
systray.go
Normal 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)
|
||||
}
|
||||
}
|
||||
306
weather.go
Normal file
306
weather.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const qweatherKey = "3b67b65a53c04170b602d2d1a7e6096f"
|
||||
const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com"
|
||||
|
||||
type City struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Adm2 string `json:"adm2"`
|
||||
Adm1 string `json:"adm1"`
|
||||
}
|
||||
|
||||
var cities = []City{
|
||||
{"101010100", "北京", "北京", "北京"},
|
||||
{"101020100", "上海", "上海", "上海"},
|
||||
{"101280101", "广州", "广州", "广东"},
|
||||
{"101280601", "深圳", "深圳", "广东"},
|
||||
{"101200101", "武汉", "武汉", "湖北"},
|
||||
{"101110101", "西安", "西安", "陕西"},
|
||||
{"101210101", "杭州", "杭州", "浙江"},
|
||||
{"101190101", "南京", "南京", "江苏"},
|
||||
{"101070101", "沈阳", "沈阳", "辽宁"},
|
||||
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
|
||||
{"101250101", "长沙", "长沙", "湖南"},
|
||||
{"101270101", "成都", "成都", "四川"},
|
||||
}
|
||||
|
||||
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
var weatherIcons = map[string]string{
|
||||
"晴": "☀️", "多云": "⛅", "阴": "☁️",
|
||||
"雨": "🌧️", "雪": "❄️", "雷": "⛈️",
|
||||
"雾": "🌫️", "霾": "😷", "风": "💨",
|
||||
}
|
||||
|
||||
func getWeatherIcon(text string) string {
|
||||
for k, v := range weatherIcons {
|
||||
if strings.Contains(text, k) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "🌤️"
|
||||
}
|
||||
|
||||
func httpGet(url string) ([]byte, error) {
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// ── 定位 ──
|
||||
|
||||
func getLocation() City {
|
||||
if city := locateByIPIP(); city != nil {
|
||||
return *city
|
||||
}
|
||||
if city := locateByQWeather(); city != nil {
|
||||
return *city
|
||||
}
|
||||
log.Println("所有定位失败,使用默认城市:", defaultCity.Name)
|
||||
return defaultCity
|
||||
}
|
||||
|
||||
func locateByIPIP() *City {
|
||||
data, err := httpGet("https://myip.ipip.net")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
text := string(data)
|
||||
re := regexp.MustCompile(`来自于[::]\s*(.+?)\s*$`)
|
||||
m := re.FindStringSubmatch(text)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Fields(m[1])
|
||||
cityName := parts[0]
|
||||
if len(parts) >= 3 {
|
||||
cityName = parts[2]
|
||||
} else if len(parts) >= 2 {
|
||||
cityName = parts[1]
|
||||
}
|
||||
for _, c := range cities {
|
||||
if c.Name == cityName {
|
||||
log.Println("ipip.net 匹配:", c.Name)
|
||||
return &c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func locateByQWeather() *City {
|
||||
data, err := httpGet("https://myip.ipip.net")
|
||||
if err == nil {
|
||||
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
|
||||
if m := re.FindStringSubmatch(string(data)); m != nil {
|
||||
return geoLookup(m[1])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func geoLookup(ip string) *City {
|
||||
url := fmt.Sprintf(qweatherHost+"/v2/city/lookup?location=%s&key=%s", ip, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Location []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Adm2 string `json:"adm2"`
|
||||
Adm1 string `json:"adm1"`
|
||||
} `json:"location"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 {
|
||||
return nil
|
||||
}
|
||||
loc := resp.Location[0]
|
||||
for _, c := range cities {
|
||||
if c.Name == loc.Name {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1}
|
||||
}
|
||||
|
||||
func getCurrentCity() City {
|
||||
cfg := loadConfig()
|
||||
if cfg.City != "" {
|
||||
for _, c := range cities {
|
||||
if c.ID == cfg.City {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return getLocation()
|
||||
}
|
||||
|
||||
// ── 天气数据 ──
|
||||
|
||||
type hourlyItem struct {
|
||||
Time string `json:"time"`
|
||||
Temp string `json:"temp"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type dailyItem struct {
|
||||
Date string `json:"date"`
|
||||
Icon string `json:"icon"`
|
||||
TempMin string `json:"tempMin"`
|
||||
TempMax string `json:"tempMax"`
|
||||
}
|
||||
|
||||
type currentWeather struct {
|
||||
Text string `json:"text"`
|
||||
Temp string `json:"temp"`
|
||||
}
|
||||
|
||||
func fetchAndPushWeather(city City) {
|
||||
type weatherData struct {
|
||||
Current string `json:"current"`
|
||||
Hourly []hourlyItem `json:"hourly"`
|
||||
Daily []dailyItem `json:"daily"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
wd := weatherData{}
|
||||
cityName := city.Name
|
||||
if city.Adm2 != city.Name {
|
||||
cityName = city.Adm2 + " " + city.Name
|
||||
}
|
||||
wd.City = cityName
|
||||
|
||||
if now := fetchCurrentWeather(city.ID); now != nil {
|
||||
icon := getWeatherIcon(now.Text)
|
||||
wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp)
|
||||
} else {
|
||||
wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName)
|
||||
}
|
||||
|
||||
wd.Hourly = fetchHourlyForecast(city.ID)
|
||||
wd.Daily = fetchDailyForecast(city.ID)
|
||||
|
||||
jsonData, _ := json.Marshal(wd)
|
||||
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
|
||||
log.Println("天气已推送:", wd.Current)
|
||||
}
|
||||
|
||||
func fetchCurrentWeather(cityID string) *currentWeather {
|
||||
url := fmt.Sprintf(qweatherHost+"/v7/weather/now?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Now struct {
|
||||
Text string `json:"text"`
|
||||
Temp string `json:"temp"`
|
||||
} `json:"now"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
return ¤tWeather{resp.Now.Text, resp.Now.Temp}
|
||||
}
|
||||
|
||||
func fetchHourlyForecast(cityID string) []hourlyItem {
|
||||
url := fmt.Sprintf(qweatherHost+"/v7/weather/24h?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Hourly []struct {
|
||||
FxTime string `json:"fxTime"`
|
||||
Temp string `json:"temp"`
|
||||
Text string `json:"text"`
|
||||
} `json:"hourly"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
var result []hourlyItem
|
||||
for i, h := range resp.Hourly {
|
||||
if i >= 8 {
|
||||
break
|
||||
}
|
||||
t := h.FxTime
|
||||
if idx := strings.Index(t, "T"); idx >= 0 {
|
||||
t = t[idx+1:]
|
||||
if len(t) >= 5 {
|
||||
t = t[:5]
|
||||
}
|
||||
}
|
||||
result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text)})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchDailyForecast(cityID string) []dailyItem {
|
||||
url := fmt.Sprintf(qweatherHost+"/v7/weather/7d?location=%s&key=%s", cityID, qweatherKey)
|
||||
data, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var resp struct {
|
||||
Code string `json:"code"`
|
||||
Daily []struct {
|
||||
FxDate string `json:"fxDate"`
|
||||
TextDay string `json:"textDay"`
|
||||
TempMin string `json:"tempMin"`
|
||||
TempMax string `json:"tempMax"`
|
||||
} `json:"daily"`
|
||||
}
|
||||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||||
return nil
|
||||
}
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var result []dailyItem
|
||||
for _, d := range resp.Daily {
|
||||
date := d.FxDate
|
||||
if date == today {
|
||||
date = "今天"
|
||||
} else {
|
||||
if t, err := time.Parse("2006-01-02", date); err == nil {
|
||||
date = weekdays[t.Weekday()]
|
||||
}
|
||||
}
|
||||
result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func weatherLoop() {
|
||||
time.Sleep(3 * time.Second)
|
||||
city := getCurrentCity()
|
||||
log.Println("初始城市:", city.Name)
|
||||
fetchAndPushWeather(city)
|
||||
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
for range ticker.C {
|
||||
city := getCurrentCity()
|
||||
fetchAndPushWeather(city)
|
||||
}
|
||||
}
|
||||
84
win32.go
Normal file
84
win32.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"unsafe"
|
||||
|
||||
"github.com/jchv/go-webview2"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//go:embed web/wallpaper.html
|
||||
var fs embed.FS
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
procFindWindowW = user32.NewProc("FindWindowW")
|
||||
procFindWindowExW = user32.NewProc("FindWindowExW")
|
||||
procSendMessageTimeoutW = user32.NewProc("SendMessageTimeoutW")
|
||||
procSetParent = user32.NewProc("SetParent")
|
||||
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
procMoveWindow = user32.NewProc("MoveWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procPostMessageW = user32.NewProc("PostMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||
)
|
||||
|
||||
var wv webview2.WebView
|
||||
var wvHwnd uintptr
|
||||
var jsQueue = make(chan string, 64)
|
||||
var paused int32
|
||||
|
||||
const wmEvalJS = 0x0401
|
||||
|
||||
func evalJS(js string) {
|
||||
select {
|
||||
case jsQueue <- js:
|
||||
log.Println("evalJS queued:", js[:min(len(js), 60)])
|
||||
default:
|
||||
log.Println("jsQueue full, dropping eval")
|
||||
}
|
||||
if wvHwnd != 0 {
|
||||
procPostMessageW.Call(wvHwnd, wmEvalJS, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if progman == 0 {
|
||||
return 0
|
||||
}
|
||||
var result uintptr
|
||||
procSendMessageTimeoutW.Call(progman, 0x052C, 0, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&result)))
|
||||
shellDefView, _, _ := procFindWindowExW.Call(progman, 0,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView"))), 0)
|
||||
workerwAfterShell, _, _ := procFindWindowExW.Call(progman, shellDefView,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
if workerwAfterShell != 0 {
|
||||
return workerwAfterShell
|
||||
}
|
||||
ww, _, _ := procFindWindowExW.Call(progman, 0,
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
return ww
|
||||
}
|
||||
|
||||
func getScreenSize() (int32, int32) {
|
||||
w, _, _ := procGetSystemMetrics.Call(0)
|
||||
h, _, _ := procGetSystemMetrics.Call(1)
|
||||
return int32(w), int32(h)
|
||||
}
|
||||
Reference in New Issue
Block a user