重构: 拆分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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"embed"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"sync/atomic"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/getlantern/systray"
|
"github.com/getlantern/systray"
|
||||||
"github.com/jchv/go-webview2"
|
|
||||||
"golang.org/x/sys/windows"
|
"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() {
|
func main() {
|
||||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||||
|
|
||||||
// 单实例互斥锁,防止双托盘
|
|
||||||
mutexName, _ := windows.UTF16PtrFromString("Global\\u-desktop-single-instance")
|
mutexName, _ := windows.UTF16PtrFromString("Global\\u-desktop-single-instance")
|
||||||
mutex, err := windows.CreateMutex(nil, false, mutexName)
|
mutex, err := windows.CreateMutex(nil, false, mutexName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("创建互斥锁失败:", err)
|
log.Fatal("创建互斥锁失败:", err)
|
||||||
}
|
}
|
||||||
if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS {
|
if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS {
|
||||||
log.Println("⚠️ 已有实例运行,退出")
|
log.Println("已有实例运行,退出")
|
||||||
windows.CloseHandle(mutex)
|
windows.CloseHandle(mutex)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,618 +31,3 @@ func main() {
|
|||||||
procSetProcessDPIAware.Call()
|
procSetProcessDPIAware.Call()
|
||||||
systray.Run(onSystrayReady, nil)
|
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