新增: 壁纸切换(主题/本地图片/Bing/纯色渐变)
This commit is contained in:
82
bing.go
Normal file
82
bing.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN"
|
||||
|
||||
type bingResponse struct {
|
||||
Images []struct {
|
||||
URL string `json:"url"`
|
||||
URLBase string `json:"urlbase"`
|
||||
Copyright string `json:"copyright"`
|
||||
} `json:"images"`
|
||||
}
|
||||
|
||||
func fetchBingWallpaper() {
|
||||
resp, err := httpClient.Get(bingAPI)
|
||||
if err != nil {
|
||||
log.Println("Bing API 请求失败:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var br bingResponse
|
||||
if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
|
||||
log.Println("Bing API 解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
imgURL := br.Images[0].URL
|
||||
if !strings.HasPrefix(imgURL, "http") {
|
||||
imgURL = "https://www.bing.com" + imgURL
|
||||
}
|
||||
|
||||
imgResp, err := httpClient.Get(imgURL)
|
||||
if err != nil {
|
||||
log.Println("Bing 图片下载失败:", err)
|
||||
return
|
||||
}
|
||||
defer imgResp.Body.Close()
|
||||
imgData, err := io.ReadAll(imgResp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
|
||||
if err := os.WriteFile(bingPath, imgData, 0644); err != nil {
|
||||
log.Println("Bing 壁纸缓存失败:", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Bing 壁纸已缓存: %s (%d bytes)", bingPath, len(imgData))
|
||||
|
||||
reloadWallpaper()
|
||||
}
|
||||
|
||||
func bingWallpaperLoop() {
|
||||
cfg := loadConfig()
|
||||
if cfg.WallpaperType == WPBing {
|
||||
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
|
||||
if _, err := os.Stat(bingPath); err != nil {
|
||||
fetchBingWallpaper()
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(4 * time.Hour)
|
||||
for range ticker.C {
|
||||
cfg := loadConfig()
|
||||
if cfg.WallpaperType == WPBing {
|
||||
fetchBingWallpaper()
|
||||
}
|
||||
}
|
||||
}
|
||||
56
config.go
56
config.go
@@ -8,32 +8,80 @@ import (
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type WallpaperType string
|
||||
|
||||
const (
|
||||
WPTheme WallpaperType = "theme"
|
||||
WPImage WallpaperType = "image"
|
||||
WPBing WallpaperType = "bing"
|
||||
WPColor WallpaperType = "color"
|
||||
)
|
||||
|
||||
type ThemeName string
|
||||
|
||||
const (
|
||||
ThemeAurora ThemeName = "aurora"
|
||||
ThemeStar ThemeName = "starfield"
|
||||
ThemeGradient ThemeName = "gradient"
|
||||
ThemeParticle ThemeName = "particles"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Zodiac string `json:"zodiac"`
|
||||
City string `json:"city"`
|
||||
Zodiac string `json:"zodiac"`
|
||||
City string `json:"city"`
|
||||
WallpaperType WallpaperType `json:"wallpaperType"`
|
||||
Theme ThemeName `json:"theme"`
|
||||
ImagePath string `json:"imagePath"`
|
||||
Color1 string `json:"color1"`
|
||||
Color2 string `json:"color2"`
|
||||
ColorGradient bool `json:"colorGradient"`
|
||||
}
|
||||
|
||||
const defaultZodiac = "射手座"
|
||||
|
||||
var configPath string
|
||||
|
||||
func configDir() string {
|
||||
return filepath.Dir(configPath)
|
||||
}
|
||||
|
||||
func loadConfig() *Config {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
return defaultConfig()
|
||||
}
|
||||
var cfg Config
|
||||
if json.Unmarshal(data, &cfg) != nil {
|
||||
return &Config{Zodiac: defaultZodiac}
|
||||
return defaultConfig()
|
||||
}
|
||||
if cfg.Zodiac == "" {
|
||||
cfg.Zodiac = defaultZodiac
|
||||
}
|
||||
if cfg.WallpaperType == "" {
|
||||
cfg.WallpaperType = WPTheme
|
||||
}
|
||||
if cfg.Theme == "" {
|
||||
cfg.Theme = ThemeAurora
|
||||
}
|
||||
if cfg.Color1 == "" {
|
||||
cfg.Color1 = "#1a1a2e"
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Zodiac: defaultZodiac,
|
||||
WallpaperType: WPTheme,
|
||||
Theme: ThemeAurora,
|
||||
Color1: "#1a1a2e",
|
||||
Color2: "#16213e",
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfig(cfg *Config) error {
|
||||
data, _ := json.MarshalIndent(cfg, "", " ")
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
|
||||
119
dialog.go
Normal file
119
dialog.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
comdlg32 = windows.NewLazySystemDLL("comdlg32.dll")
|
||||
procGetOpenFileNameW = comdlg32.NewProc("GetOpenFileNameW")
|
||||
procChooseColorW = comdlg32.NewProc("ChooseColorW")
|
||||
)
|
||||
|
||||
func slicePtr(s interface{}) uintptr {
|
||||
switch v := s.(type) {
|
||||
case []uint16:
|
||||
if len(v) == 0 { return 0 }
|
||||
return uintptr(unsafe.Pointer(&v[0]))
|
||||
case []uint32:
|
||||
if len(v) == 0 { return 0 }
|
||||
return uintptr(unsafe.Pointer(&v[0]))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func openFileDialog(owner uintptr) string {
|
||||
type openFileName struct {
|
||||
lStructSize uint32
|
||||
hwndOwner uintptr
|
||||
hInstance uintptr
|
||||
lpstrFilter uintptr
|
||||
lpstrCustomFilter uintptr
|
||||
nMaxCustFilter uint32
|
||||
nFilterIndex uint32
|
||||
lpstrFile uintptr
|
||||
nMaxFile uint32
|
||||
lpstrFileTitle uintptr
|
||||
nMaxFileTitle uint32
|
||||
lpstrInitialDir uintptr
|
||||
lpstrTitle uintptr
|
||||
Flags uint32
|
||||
nFileOffset uint16
|
||||
nFileExtension uint16
|
||||
lpstrDefExt uintptr
|
||||
lCustData uintptr
|
||||
lpfnHook uintptr
|
||||
lpTemplateName uintptr
|
||||
pvReserved uintptr
|
||||
dwReserved uint32
|
||||
FlagsEx uint32
|
||||
}
|
||||
|
||||
// Filter: "Images\0*.jpg;*.png\0All\0*.*\0\0"
|
||||
filterStr := "图片文件\x00*.jpg;*.jpeg;*.png;*.bmp;*.webp;*.gif\x00所有文件\x00*.*\x00\x00"
|
||||
filterUTF16 := utf16.Encode([]rune(filterStr))
|
||||
|
||||
titleUTF16 := utf16.Encode([]rune("选择壁纸图片"))
|
||||
fileBuf := make([]uint16, 260)
|
||||
|
||||
ofn := openFileName{
|
||||
lStructSize: uint32(unsafe.Sizeof(openFileName{})),
|
||||
hwndOwner: owner,
|
||||
lpstrFilter: slicePtr(filterUTF16),
|
||||
lpstrFile: slicePtr(fileBuf),
|
||||
nMaxFile: 260,
|
||||
lpstrTitle: slicePtr(titleUTF16),
|
||||
Flags: 0x00001000 | 0x00000800 | 0x00000004,
|
||||
}
|
||||
|
||||
ret, _, _ := procGetOpenFileNameW.Call(uintptr(unsafe.Pointer(&ofn)))
|
||||
if ret == 0 {
|
||||
return ""
|
||||
}
|
||||
return windows.UTF16ToString(fileBuf)
|
||||
}
|
||||
|
||||
func colorPickerDialog(owner uintptr, initialColor string) string {
|
||||
type chooseColor struct {
|
||||
lStructSize uint32
|
||||
hwndOwner uintptr
|
||||
hInstance uintptr
|
||||
rgbResult uint32
|
||||
lpCustColors uintptr
|
||||
Flags uint32
|
||||
lCustData uintptr
|
||||
lpfnHook uintptr
|
||||
lpTemplateName uintptr
|
||||
}
|
||||
|
||||
custColors := make([]uint32, 16)
|
||||
|
||||
var initRGB uint32
|
||||
if len(initialColor) > 4 && initialColor[0] == '#' {
|
||||
var r, g, b uint32
|
||||
fmt.Sscanf(initialColor[1:], "%02x%02x%02x", &r, &g, &b)
|
||||
initRGB = r | (g << 8) | (b << 16)
|
||||
}
|
||||
|
||||
cc := chooseColor{
|
||||
lStructSize: uint32(unsafe.Sizeof(chooseColor{})),
|
||||
hwndOwner: owner,
|
||||
rgbResult: initRGB,
|
||||
lpCustColors: slicePtr(custColors),
|
||||
Flags: 0x00000002,
|
||||
}
|
||||
|
||||
ret, _, _ := procChooseColorW.Call(uintptr(unsafe.Pointer(&cc)))
|
||||
if ret == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
r := cc.rgbResult & 0xFF
|
||||
g := (cc.rgbResult >> 8) & 0xFF
|
||||
b := (cc.rgbResult >> 16) & 0xFF
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
6
main.go
6
main.go
@@ -25,9 +25,9 @@ func main() {
|
||||
defer windows.CloseHandle(mutex)
|
||||
|
||||
exePath, _ := os.Executable()
|
||||
configDir := filepath.Join(filepath.Dir(exePath), "config")
|
||||
os.MkdirAll(configDir, 0755)
|
||||
configPath = filepath.Join(configDir, "settings.json")
|
||||
cfgDir := filepath.Join(filepath.Dir(exePath), "config")
|
||||
os.MkdirAll(cfgDir, 0755)
|
||||
configPath = filepath.Join(cfgDir, "settings.json")
|
||||
procSetProcessDPIAware.Call()
|
||||
systray.Run(onSystrayReady, nil)
|
||||
}
|
||||
|
||||
162
systray.go
162
systray.go
@@ -16,14 +16,44 @@ import (
|
||||
|
||||
var zodiacItems []*systray.MenuItem
|
||||
var cityItems []*systray.MenuItem
|
||||
var themeItems []*systray.MenuItem
|
||||
|
||||
var themeNames = []struct {
|
||||
Name ThemeName
|
||||
Label string
|
||||
}{
|
||||
{ThemeAurora, "极光"},
|
||||
{ThemeStar, "星空"},
|
||||
{ThemeGradient, "渐变"},
|
||||
{ThemeParticle, "粒子"},
|
||||
}
|
||||
|
||||
func onSystrayReady() {
|
||||
systray.SetIcon(generateIcon())
|
||||
systray.SetTooltip("动态壁纸引擎")
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
mPause := systray.AddMenuItem("暂停", "暂停/继续")
|
||||
systray.AddSeparator()
|
||||
|
||||
// 壁纸主题
|
||||
mTheme := systray.AddMenuItem("壁纸主题", "")
|
||||
for _, t := range themeNames {
|
||||
item := mTheme.AddSubMenuItem(t.Label, t.Label)
|
||||
if cfg.WallpaperType == WPTheme && cfg.Theme == t.Name {
|
||||
item.Check()
|
||||
}
|
||||
themeItems = append(themeItems, item)
|
||||
}
|
||||
mLocalImage := systray.AddMenuItem("本地图片", "选择本地图片作为壁纸")
|
||||
mBingDaily := systray.AddMenuItem("Bing 每日壁纸", "使用 Bing 每日壁纸")
|
||||
mSolidColor := systray.AddMenuItem("纯色壁纸", "选择纯色壁纸")
|
||||
mGradientColor := systray.AddMenuItem("渐变壁纸", "选择渐变壁纸")
|
||||
|
||||
systray.AddSeparator()
|
||||
|
||||
// 星座
|
||||
mZodiac := systray.AddMenuItem("星座设置", "")
|
||||
zodiacs := []string{
|
||||
"白羊座", "金牛座", "双子座",
|
||||
@@ -31,7 +61,6 @@ func onSystrayReady() {
|
||||
"天秤座", "天蝎座", "射手座",
|
||||
"摩羯座", "水瓶座", "双鱼座",
|
||||
}
|
||||
cfg := loadConfig()
|
||||
for _, z := range zodiacs {
|
||||
item := mZodiac.AddSubMenuItem(z, z)
|
||||
if z == cfg.Zodiac {
|
||||
@@ -42,6 +71,7 @@ func onSystrayReady() {
|
||||
|
||||
systray.AddSeparator()
|
||||
|
||||
// 城市
|
||||
mCity := systray.AddMenuItem("城市设置", "")
|
||||
for _, c := range cities {
|
||||
item := mCity.AddSubMenuItem(c.Name, c.Adm1+" "+c.Name)
|
||||
@@ -54,14 +84,113 @@ func onSystrayReady() {
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("退出", "退出程序")
|
||||
|
||||
// 主题切换监听
|
||||
for i, item := range themeItems {
|
||||
go func(idx int, mi *systray.MenuItem) {
|
||||
for {
|
||||
<-mi.ClickedCh
|
||||
cfg := loadConfig()
|
||||
cfg.WallpaperType = WPTheme
|
||||
cfg.Theme = themeNames[idx].Name
|
||||
saveConfig(cfg)
|
||||
for _, it := range themeItems {
|
||||
it.Uncheck()
|
||||
}
|
||||
mi.Check()
|
||||
log.Printf("主题切换: %s", themeNames[idx].Label)
|
||||
reloadWallpaper()
|
||||
}
|
||||
}(i, item)
|
||||
}
|
||||
|
||||
// 本地图片
|
||||
go func() {
|
||||
for {
|
||||
<-mLocalImage.ClickedCh
|
||||
path := openFileDialog(wvHwnd)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
cfg := loadConfig()
|
||||
cfg.WallpaperType = WPImage
|
||||
cfg.ImagePath = path
|
||||
saveConfig(cfg)
|
||||
for _, it := range themeItems {
|
||||
it.Uncheck()
|
||||
}
|
||||
log.Printf("本地图片: %s", path)
|
||||
reloadWallpaper()
|
||||
}
|
||||
}()
|
||||
|
||||
// Bing 每日
|
||||
go func() {
|
||||
for {
|
||||
<-mBingDaily.ClickedCh
|
||||
cfg := loadConfig()
|
||||
cfg.WallpaperType = WPBing
|
||||
saveConfig(cfg)
|
||||
for _, it := range themeItems {
|
||||
it.Uncheck()
|
||||
}
|
||||
log.Println("切换 Bing 壁纸")
|
||||
go fetchBingWallpaper()
|
||||
}
|
||||
}()
|
||||
|
||||
// 纯色壁纸
|
||||
go func() {
|
||||
for {
|
||||
<-mSolidColor.ClickedCh
|
||||
color := colorPickerDialog(wvHwnd, "")
|
||||
if color == "" {
|
||||
continue
|
||||
}
|
||||
cfg := loadConfig()
|
||||
cfg.WallpaperType = WPColor
|
||||
cfg.Color1 = color
|
||||
cfg.ColorGradient = false
|
||||
saveConfig(cfg)
|
||||
for _, it := range themeItems {
|
||||
it.Uncheck()
|
||||
}
|
||||
log.Printf("纯色壁纸: %s", color)
|
||||
reloadWallpaper()
|
||||
}
|
||||
}()
|
||||
|
||||
// 渐变壁纸
|
||||
go func() {
|
||||
for {
|
||||
<-mGradientColor.ClickedCh
|
||||
c1 := colorPickerDialog(wvHwnd, "")
|
||||
if c1 == "" {
|
||||
continue
|
||||
}
|
||||
c2 := colorPickerDialog(wvHwnd, "")
|
||||
if c2 == "" {
|
||||
c2 = "#16213e"
|
||||
}
|
||||
cfg := loadConfig()
|
||||
cfg.WallpaperType = WPColor
|
||||
cfg.Color1 = c1
|
||||
cfg.Color2 = c2
|
||||
cfg.ColorGradient = true
|
||||
saveConfig(cfg)
|
||||
for _, it := range themeItems {
|
||||
it.Uncheck()
|
||||
}
|
||||
log.Printf("渐变壁纸: %s -> %s", c1, c2)
|
||||
reloadWallpaper()
|
||||
}
|
||||
}()
|
||||
|
||||
// 星座选择监听
|
||||
for i, item := range zodiacItems {
|
||||
go func(idx int, mi *systray.MenuItem) {
|
||||
name := zodiacs[idx]
|
||||
log.Printf("星座监听启动: %s", name)
|
||||
for {
|
||||
<-mi.ClickedCh
|
||||
log.Printf("星座点击: %s", name)
|
||||
cfg := loadConfig()
|
||||
cfg.Zodiac = name
|
||||
saveConfig(cfg)
|
||||
@@ -77,11 +206,9 @@ func onSystrayReady() {
|
||||
// 城市选择监听
|
||||
for i, item := range cityItems {
|
||||
go func(idx int, mi *systray.MenuItem) {
|
||||
log.Printf("城市监听启动: %s", cities[idx].Name)
|
||||
for {
|
||||
<-mi.ClickedCh
|
||||
city := cities[idx]
|
||||
log.Printf("城市点击: %s", city.Name)
|
||||
cfg := loadConfig()
|
||||
cfg.City = city.ID
|
||||
saveConfig(cfg)
|
||||
@@ -96,13 +223,11 @@ func onSystrayReady() {
|
||||
|
||||
// 暂停
|
||||
go func() {
|
||||
log.Println("暂停监听启动")
|
||||
for {
|
||||
<-mPause.ClickedCh
|
||||
newVal := 1 - atomic.LoadInt32(&paused)
|
||||
atomic.StoreInt32(&paused, newVal)
|
||||
isPaused := newVal == 1
|
||||
log.Printf("暂停切换: paused=%v", isPaused)
|
||||
if isPaused {
|
||||
mPause.SetTitle("继续")
|
||||
} else {
|
||||
@@ -120,6 +245,7 @@ func onSystrayReady() {
|
||||
|
||||
go startWebView()
|
||||
go weatherLoop()
|
||||
go bingWallpaperLoop()
|
||||
}
|
||||
|
||||
func startWebView() {
|
||||
@@ -145,18 +271,13 @@ func startWebView() {
|
||||
log.Fatal("WebView2 create failed")
|
||||
}
|
||||
|
||||
htmlData, err := fs.ReadFile("web/wallpaper.html")
|
||||
if err != nil {
|
||||
log.Fatal("读取 wallpaper.html 失败:", err)
|
||||
}
|
||||
|
||||
wv.Bind("setZodiacFromGo", func(zodiac string) error {
|
||||
cfg := loadConfig()
|
||||
cfg.Zodiac = zodiac
|
||||
return saveConfig(cfg)
|
||||
})
|
||||
|
||||
wv.SetHtml(string(htmlData))
|
||||
wv.SetHtml(buildWallpaperHTML(loadConfig()))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
wvHwnd = uintptr(wv.Window())
|
||||
@@ -165,17 +286,14 @@ func startWebView() {
|
||||
procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
|
||||
log.Printf("壁纸已嵌入: HWND=0x%x, %dx%d", wvHwnd, screenW, screenH)
|
||||
|
||||
// 注入配置
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
cfg := loadConfig()
|
||||
evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
|
||||
log.Printf("配置已注入: zodiac=%s", cfg.Zodiac)
|
||||
}()
|
||||
|
||||
go fullscreenMonitor()
|
||||
|
||||
// 延迟嵌入 WorkerW
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
workerw := findWorkerW()
|
||||
@@ -186,7 +304,6 @@ func startWebView() {
|
||||
}
|
||||
}()
|
||||
|
||||
// 消息循环
|
||||
type msg struct {
|
||||
hwnd uintptr
|
||||
message uint32
|
||||
@@ -196,7 +313,6 @@ func startWebView() {
|
||||
pt struct{ x, y int32 }
|
||||
}
|
||||
var m msg
|
||||
log.Println("启动自定义消息循环...")
|
||||
for {
|
||||
ret, _, _ := procGetMessageW.Call(
|
||||
uintptr(unsafe.Pointer(&m)),
|
||||
@@ -215,7 +331,15 @@ func startWebView() {
|
||||
}
|
||||
}
|
||||
}
|
||||
nextMsg:
|
||||
if m.message == wmSetHtml {
|
||||
select {
|
||||
case html := <-htmlQueue:
|
||||
wv.SetHtml(html)
|
||||
default:
|
||||
}
|
||||
goto nextMsg
|
||||
}
|
||||
nextMsg:
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||||
}
|
||||
|
||||
113
wallpaper.go
Normal file
113
wallpaper.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed web/overlay.html
|
||||
var overlayHTML string
|
||||
|
||||
//go:embed web/themes/aurora.html
|
||||
var themeAurora string
|
||||
|
||||
//go:embed web/themes/starfield.html
|
||||
var themeStarfield string
|
||||
|
||||
//go:embed web/themes/gradient.html
|
||||
var themeGradient string
|
||||
|
||||
//go:embed web/themes/particles.html
|
||||
var themeParticles string
|
||||
|
||||
var themeMap = map[ThemeName]string{
|
||||
ThemeAurora: themeAurora,
|
||||
ThemeStar: themeStarfield,
|
||||
ThemeGradient: themeGradient,
|
||||
ThemeParticle: themeParticles,
|
||||
}
|
||||
|
||||
func buildWallpaperHTML(cfg *Config) string {
|
||||
var bg string
|
||||
|
||||
switch cfg.WallpaperType {
|
||||
case WPTheme:
|
||||
if t, ok := themeMap[cfg.Theme]; ok {
|
||||
bg = t
|
||||
} else {
|
||||
bg = themeAurora
|
||||
}
|
||||
case WPImage:
|
||||
if cfg.ImagePath != "" {
|
||||
src := imageToDataURI(cfg.ImagePath)
|
||||
if src != "" {
|
||||
bg = fmt.Sprintf(`<img src="%s" style="position:fixed;top:0;left:0;width:100%%;height:100%%;object-fit:cover;z-index:1;">`, src)
|
||||
}
|
||||
}
|
||||
case WPBing:
|
||||
bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
|
||||
if _, err := os.Stat(bingPath); err == nil {
|
||||
src := imageToDataURI(bingPath)
|
||||
if src != "" {
|
||||
bg = fmt.Sprintf(`<img src="%s" style="position:fixed;top:0;left:0;width:100%%;height:100%%;object-fit:cover;z-index:1;">`, src)
|
||||
}
|
||||
}
|
||||
case WPColor:
|
||||
if cfg.ColorGradient && cfg.Color2 != "" {
|
||||
bg = fmt.Sprintf(`<div style="position:fixed;top:0;left:0;width:100%%;height:100%%;z-index:1;background:linear-gradient(135deg,%s,%s);"></div>`, cfg.Color1, cfg.Color2)
|
||||
} else {
|
||||
bg = fmt.Sprintf(`<div style="position:fixed;top:0;left:0;width:100%%;height:100%%;z-index:1;background:%s;"></div>`, cfg.Color1)
|
||||
}
|
||||
}
|
||||
|
||||
if bg == "" {
|
||||
bg = themeAurora
|
||||
}
|
||||
return strings.Replace(overlayHTML, "{{BACKGROUND}}", bg, 1)
|
||||
}
|
||||
|
||||
func imageToDataURI(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Println("读取图片失败:", err)
|
||||
return ""
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
mime := "image/jpeg"
|
||||
switch ext {
|
||||
case ".png":
|
||||
mime = "image/png"
|
||||
case ".gif":
|
||||
mime = "image/gif"
|
||||
case ".webp":
|
||||
mime = "image/webp"
|
||||
case ".bmp":
|
||||
mime = "image/bmp"
|
||||
}
|
||||
return fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(data))
|
||||
}
|
||||
|
||||
func reloadWallpaper() {
|
||||
if wv == nil || wvHwnd == 0 {
|
||||
return
|
||||
}
|
||||
cfg := loadConfig()
|
||||
html := buildWallpaperHTML(cfg)
|
||||
select {
|
||||
case htmlQueue <- html:
|
||||
default:
|
||||
}
|
||||
procPostMessageW.Call(wvHwnd, wmSetHtml, 0, 0)
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
|
||||
city := getCurrentCity()
|
||||
go fetchAndPushWeather(city)
|
||||
}()
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func getLocation() City {
|
||||
if city := locateByIPIP(); city != nil {
|
||||
return *city
|
||||
}
|
||||
if city := locateByQWeather(); city != nil {
|
||||
if city := locateByGeoAPI(); city != nil {
|
||||
return *city
|
||||
}
|
||||
log.Println("所有定位失败,使用默认城市:", defaultCity.Name)
|
||||
@@ -104,7 +104,7 @@ func locateByIPIP() *City {
|
||||
return nil
|
||||
}
|
||||
|
||||
func locateByQWeather() *City {
|
||||
func locateByGeoAPI() *City {
|
||||
data, err := httpGet("https://myip.ipip.net")
|
||||
if err == nil {
|
||||
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
|
||||
|
||||
133
web/overlay.html
Normal file
133
web/overlay.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#info {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 80px;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
color: #ffffff;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
text-align: right;
|
||||
min-width: 300px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.time { font-size: 64px; font-weight: 300; color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,0.8); letter-spacing: -1px; }
|
||||
.date { font-size: 15px; color: rgba(255,255,255,0.9); margin-top: 6px; text-shadow: 0 1px 6px rgba(0,0,0,0.8); }
|
||||
.weather-section { margin-top: 20px; }
|
||||
.current-weather { font-size: 17px; color: rgba(255,255,255,0.95); text-shadow: 0 1px 4px rgba(0,0,0,0.8); margin-bottom: 12px; }
|
||||
.forecast-title { font-size: 13px; color: rgba(255,255,255,0.7); margin-bottom: 8px; }
|
||||
.weather-forecast { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 8px; justify-content: flex-end; }
|
||||
.forecast-item { background: rgba(255,255,255,0.08); border-radius: 8px; padding: 8px 10px; text-align: center; min-width: 55px; font-size: 11px; color: rgba(255,255,255,0.85); }
|
||||
.forecast-time { margin-bottom: 4px; opacity: 0.8; }
|
||||
.forecast-temp { font-weight: 500; }
|
||||
.daily-forecast { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 8px; justify-content: flex-end; }
|
||||
.daily-item { background: rgba(255,255,255,0.06); border-radius: 6px; padding: 6px 8px; text-align: center; min-width: 48px; font-size: 11px; color: rgba(255,255,255,0.85); }
|
||||
.zodiac { margin-top: 18px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.15); font-size: 15px; color: rgba(255,255,255,0.9); line-height: 1.5; text-shadow: 0 1px 4px rgba(0,0,0,0.8); }
|
||||
.weather-forecast::-webkit-scrollbar, #info::-webkit-scrollbar { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{BACKGROUND}}
|
||||
|
||||
<div id="info">
|
||||
<div class="time" id="time">00:00</div>
|
||||
<div class="date" id="date">1月1日 周一</div>
|
||||
<div class="weather-section">
|
||||
<div class="current-weather" id="currentWeather">🌤️ 加载中...</div>
|
||||
<div class="forecast-title">未来24小时</div>
|
||||
<div class="weather-forecast" id="hourlyForecast"></div>
|
||||
<div class="forecast-title" style="margin-top: 12px;">未来7天</div>
|
||||
<div class="daily-forecast" id="dailyForecast"></div>
|
||||
</div>
|
||||
<div class="zodiac" id="zodiac">✨ 射手座运势</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let lastTimeStr = '';
|
||||
let lastDateStr = '';
|
||||
let lastZodiac = '';
|
||||
|
||||
window.updateWeatherFromGo = function(data) {
|
||||
if (typeof data === 'string') data = JSON.parse(data);
|
||||
var el = document.getElementById('currentWeather');
|
||||
if (el && data.current) el.textContent = data.current;
|
||||
var fel = document.getElementById('hourlyForecast');
|
||||
if (fel) {
|
||||
if (data.hourly && data.hourly.length > 0) {
|
||||
fel.innerHTML = data.hourly.map(function(item) {
|
||||
return '<div class="forecast-item"><div class="forecast-time">' + item.time + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.temp + '</div></div>';
|
||||
}).join('');
|
||||
} else { fel.innerHTML = '<div style="font-size:11px;opacity:0.5">暂无数据</div>'; }
|
||||
}
|
||||
var del = document.getElementById('dailyForecast');
|
||||
if (del) {
|
||||
if (data.daily && data.daily.length > 0) {
|
||||
del.innerHTML = data.daily.map(function(item) {
|
||||
return '<div class="daily-item"><div style="opacity:0.8;margin-bottom:3px">' + item.date + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.tempMin + '°~' + item.tempMax + '°</div></div>';
|
||||
}).join('');
|
||||
} else { del.innerHTML = '<div style="font-size:11px;opacity:0.5">暂无数据</div>'; }
|
||||
}
|
||||
};
|
||||
|
||||
var fortunes = {
|
||||
'白羊座':'今日运势旺盛,适合开展新计划。','金牛座':'财运不错,但需注意健康。',
|
||||
'双子座':'人际关系活跃,社交运势佳。','巨蟹座':'情绪敏感,适合独处思考。',
|
||||
'狮子座':'自信爆棚,工作表现突出。','处女座':'细节决定成败,专注当下。',
|
||||
'天秤座':'感情运佳,单身者有机会。','天蝎座':'直觉敏锐,适合做决策。',
|
||||
'射手座':'冒险精神旺盛,出行注意安全。','摩羯座':'事业运佳,工作效率高。',
|
||||
'水瓶座':'创新思维活跃,灵感不断。','双鱼座':'艺术灵感丰富,适合创作。'
|
||||
};
|
||||
|
||||
function getUserZodiac() { return window.userZodiac || '射手座'; }
|
||||
|
||||
function updateZodiacDisplay() {
|
||||
var name = getUserZodiac();
|
||||
if (name === lastZodiac) return;
|
||||
lastZodiac = name;
|
||||
var el = document.getElementById('zodiac');
|
||||
if (!el) return;
|
||||
var fortune = fortunes[name] || '运势平稳,保持平常心。';
|
||||
el.innerHTML = '✨ ' + name + '运势<br><small style="opacity:0.7">' + fortune + '</small>';
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
var now = new Date();
|
||||
var hh = String(now.getHours()).padStart(2, '0');
|
||||
var mm = String(now.getMinutes()).padStart(2, '0');
|
||||
var month = now.getMonth() + 1;
|
||||
var day = now.getDate();
|
||||
var week = ['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
|
||||
var timeStr = hh + ':' + mm;
|
||||
var dateStr = month + '月' + day + '日 ' + week;
|
||||
var timeEl = document.getElementById('time');
|
||||
var dateEl = document.getElementById('date');
|
||||
if (timeEl && timeStr !== lastTimeStr) { timeEl.textContent = timeStr; lastTimeStr = timeStr; }
|
||||
if (dateEl && dateStr !== lastDateStr) { dateEl.textContent = dateStr; lastDateStr = dateStr; }
|
||||
updateZodiacDisplay();
|
||||
}
|
||||
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
77
web/themes/aurora.html
Normal file
77
web/themes/aurora.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
|
||||
<script>
|
||||
var canvas = document.getElementById('canvas');
|
||||
var gl = canvas.getContext('webgl');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
var vsSource = 'attribute vec4 aVertexPosition;void main(){gl_Position=aVertexPosition;}';
|
||||
var fsSource = `
|
||||
precision mediump float;
|
||||
uniform float uTime;
|
||||
uniform vec2 uResolution;
|
||||
void main() {
|
||||
vec2 uv=gl_FragCoord.xy/uResolution.xy;
|
||||
vec3 c1=vec3(0.1,0.4,0.8);
|
||||
vec3 c2=vec3(0.4,0.2,0.8);
|
||||
vec3 c3=vec3(0.1,0.6,0.4);
|
||||
float w1=sin(uv.x*3.0+uTime*0.5)*0.5+0.5;
|
||||
float w2=sin(uv.x*4.0+uTime*0.3+uv.y*2.0)*0.5+0.5;
|
||||
float w3=sin(uv.x*2.0+uTime*0.7+uv.y*3.0)*0.5+0.5;
|
||||
float d=uv.y*2.0-1.0;
|
||||
float g=exp(-d*d*2.0);
|
||||
vec3 a=(c1*w1+c2*w2+c3*w3)*g*0.6;
|
||||
vec3 b=mix(vec3(0.02,0.02,0.08),vec3(0.05,0.05,0.15),uv.y);
|
||||
gl_FragColor=vec4(b+a,1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
function loadShader(gl,type,source){
|
||||
var s=gl.createShader(type);
|
||||
gl.shaderSource(s,source);
|
||||
gl.compileShader(s);
|
||||
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){gl.deleteShader(s);return null;}
|
||||
return s;
|
||||
}
|
||||
|
||||
var vs=loadShader(gl,gl.VERTEX_SHADER,vsSource);
|
||||
var fs=loadShader(gl,gl.FRAGMENT_SHADER,fsSource);
|
||||
var prog=gl.createProgram();
|
||||
gl.attachShader(prog,vs);
|
||||
gl.attachShader(prog,fs);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
var posLoc=gl.getAttribLocation(prog,'aVertexPosition');
|
||||
var timeLoc=gl.getUniformLocation(prog,'uTime');
|
||||
var resLoc=gl.getUniformLocation(prog,'uResolution');
|
||||
|
||||
var posBuf=gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,1,1,1,-1,-1,1,-1]),gl.STATIC_DRAW);
|
||||
|
||||
var startTime=Date.now();
|
||||
var FPS=15,lastFrame=0;
|
||||
|
||||
function render(ts){
|
||||
requestAnimationFrame(render);
|
||||
if(ts-lastFrame<1000/FPS)return;
|
||||
lastFrame=ts;
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
gl.clearColor(0,0,0,1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(prog);
|
||||
gl.enableVertexAttribArray(posLoc);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
|
||||
gl.vertexAttribPointer(posLoc,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform1f(timeLoc,(Date.now()-startTime)/1000);
|
||||
gl.uniform2f(resLoc,canvas.width,canvas.height);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
</script>
|
||||
17
web/themes/gradient.html
Normal file
17
web/themes/gradient.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<style>
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
#gradient-bg {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1;
|
||||
background: linear-gradient(-45deg, #0f0c29, #302b63, #24243e, #1a1a2e, #16213e);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 20s ease infinite;
|
||||
}
|
||||
</style>
|
||||
<div id="gradient-bg"></div>
|
||||
60
web/themes/particles.html
Normal file
60
web/themes/particles.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
|
||||
<script>
|
||||
var canvas = document.getElementById('canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
var particles = [];
|
||||
for (var i = 0; i < 80; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
r: Math.random() * 3 + 1,
|
||||
vx: (Math.random() - 0.5) * 0.5,
|
||||
vy: -(Math.random() * 0.8 + 0.2),
|
||||
alpha: Math.random() * 0.5 + 0.2,
|
||||
hue: Math.random() * 60 + 200
|
||||
});
|
||||
}
|
||||
|
||||
var FPS = 15, lastFrame = 0;
|
||||
|
||||
function render(ts) {
|
||||
requestAnimationFrame(render);
|
||||
if (ts - lastFrame < 1000 / FPS) return;
|
||||
lastFrame = ts;
|
||||
|
||||
var w = canvas.width, h = canvas.height;
|
||||
ctx.fillStyle = 'rgba(5, 5, 15, 0.15)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
for (var i = 0; i < particles.length; i++) {
|
||||
var p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
if (p.y < -20) { p.y = h + 20; p.x = Math.random() * w; }
|
||||
if (p.x < -20) p.x = w + 20;
|
||||
if (p.x > w + 20) p.x = -20;
|
||||
|
||||
var grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 4);
|
||||
grad.addColorStop(0, 'hsla(' + p.hue + ', 70%, 70%, ' + p.alpha + ')');
|
||||
grad.addColorStop(1, 'hsla(' + p.hue + ', 70%, 70%, 0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r * 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'hsla(' + p.hue + ', 80%, 80%, ' + (p.alpha + 0.3) + ')';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
</script>
|
||||
64
web/themes/starfield.html
Normal file
64
web/themes/starfield.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
|
||||
<script>
|
||||
var canvas = document.getElementById('canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
var stars = [];
|
||||
for (var i = 0; i < 300; i++) {
|
||||
stars.push({
|
||||
x: (Math.random() - 0.5) * canvas.width,
|
||||
y: (Math.random() - 0.5) * canvas.height,
|
||||
z: Math.random() * canvas.width,
|
||||
pz: 0
|
||||
});
|
||||
}
|
||||
|
||||
var FPS = 15, lastFrame = 0;
|
||||
|
||||
function render(ts) {
|
||||
requestAnimationFrame(render);
|
||||
if (ts - lastFrame < 1000 / FPS) return;
|
||||
lastFrame = ts;
|
||||
|
||||
var w = canvas.width, h = canvas.height;
|
||||
var cx = w / 2, cy = h / 2;
|
||||
ctx.fillStyle = 'rgba(2, 3, 12, 0.25)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
var speed = 8;
|
||||
for (var i = 0; i < stars.length; i++) {
|
||||
var s = stars[i];
|
||||
s.z -= speed;
|
||||
if (s.z <= 0) {
|
||||
s.x = (Math.random() - 0.5) * w;
|
||||
s.y = (Math.random() - 0.5) * h;
|
||||
s.z = w;
|
||||
s.pz = s.z;
|
||||
}
|
||||
var sx = (s.x / s.z) * w * 0.5 + cx;
|
||||
var sy = (s.y / s.z) * h * 0.5 + cy;
|
||||
var px = (s.x / s.pz) * w * 0.5 + cx;
|
||||
var py = (s.y / s.pz) * h * 0.5 + cy;
|
||||
s.pz = s.z;
|
||||
var r = Math.max(0.5, (1 - s.z / w) * 2.5);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px, py);
|
||||
ctx.lineTo(sx, sy);
|
||||
ctx.strokeStyle = 'rgba(200, 220, 255, ' + ((1 - s.z / w) * 0.8) + ')';
|
||||
ctx.lineWidth = r;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, r * 0.6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(220, 235, 255, ' + ((1 - s.z / w) * 0.9) + ')';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
</script>
|
||||
@@ -1,356 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>动态壁纸</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#info {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 80px;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
color: #ffffff;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
text-align: right;
|
||||
min-width: 300px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 64px;
|
||||
font-weight: 300;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8);
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 6px;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.weather-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.current-weather {
|
||||
font-size: 17px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 400;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.forecast-title {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.weather-forecast {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.forecast-item {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
min-width: 55px;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.forecast-time {
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.forecast-temp {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.daily-forecast {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.daily-item {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
min-width: 48px;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.zodiac {
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.5;
|
||||
cursor: default;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.current-weather {
|
||||
font-size: 17px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 400;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.weather-forecast::-webkit-scrollbar,
|
||||
#info::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div id="info">
|
||||
<div class="time" id="time">00:00</div>
|
||||
<div class="date" id="date">1月1日 周一</div>
|
||||
|
||||
<div class="weather-section">
|
||||
<div class="current-weather" id="currentWeather">🌤️ 加载中...</div>
|
||||
|
||||
<div class="forecast-title">未来24小时</div>
|
||||
<div class="weather-forecast" id="hourlyForecast"></div>
|
||||
|
||||
<div class="forecast-title" style="margin-top: 12px;">未来7天</div>
|
||||
<div class="daily-forecast" id="dailyForecast"></div>
|
||||
</div>
|
||||
|
||||
<div class="zodiac" id="zodiac">✨ 射手座运势</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('=== 页面已加载 ===');
|
||||
|
||||
let lastTimeStr = '';
|
||||
let lastDateStr = '';
|
||||
let lastZodiac = '';
|
||||
|
||||
// 天气渲染:由 Go 层推送数据
|
||||
window.updateWeatherFromGo = function(data) {
|
||||
if (typeof data === 'string') data = JSON.parse(data);
|
||||
|
||||
const currentEl = document.getElementById('currentWeather');
|
||||
if (currentEl && data.current) currentEl.textContent = data.current;
|
||||
|
||||
const forecastEl = document.getElementById('hourlyForecast');
|
||||
if (forecastEl) {
|
||||
if (data.hourly && data.hourly.length > 0) {
|
||||
forecastEl.innerHTML = data.hourly.map(item =>
|
||||
'<div class="forecast-item"><div class="forecast-time">' + item.time + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.temp + '</div></div>'
|
||||
).join('');
|
||||
} else {
|
||||
forecastEl.innerHTML = '<div style="font-size:11px; opacity:0.5;">暂无数据</div>';
|
||||
}
|
||||
}
|
||||
|
||||
const dailyEl = document.getElementById('dailyForecast');
|
||||
if (dailyEl) {
|
||||
if (data.daily && data.daily.length > 0) {
|
||||
dailyEl.innerHTML = data.daily.map(item =>
|
||||
'<div class="daily-item"><div style="opacity:0.8;margin-bottom:3px">' + item.date + '</div><div>' + item.icon + '</div><div class="forecast-temp">' + item.tempMin + '°~' + item.tempMax + '°</div></div>'
|
||||
).join('');
|
||||
} else {
|
||||
dailyEl.innerHTML = '<div style="font-size:11px; opacity:0.5;">暂无数据</div>';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 天气已更新:', data.current);
|
||||
};
|
||||
|
||||
// 星座运势
|
||||
const fortunes = {
|
||||
'白羊座': '今日运势旺盛,适合开展新计划。',
|
||||
'金牛座': '财运不错,但需注意健康。',
|
||||
'双子座': '人际关系活跃,社交运势佳。',
|
||||
'巨蟹座': '情绪敏感,适合独处思考。',
|
||||
'狮子座': '自信爆棚,工作表现突出。',
|
||||
'处女座': '细节决定成败,专注当下。',
|
||||
'天秤座': '感情运佳,单身者有机会。',
|
||||
'天蝎座': '直觉敏锐,适合做决策。',
|
||||
'射手座': '冒险精神旺盛,出行注意安全。',
|
||||
'摩羯座': '事业运佳,工作效率高。',
|
||||
'水瓶座': '创新思维活跃,灵感不断。',
|
||||
'双鱼座': '艺术灵感丰富,适合创作。'
|
||||
};
|
||||
|
||||
// WebGL 极光背景
|
||||
const canvas = document.getElementById('canvas');
|
||||
const gl = canvas.getContext('webgl');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
const vsSource = 'attribute vec4 aVertexPosition;void main(){gl_Position=aVertexPosition;}';
|
||||
const fsSource = `
|
||||
precision mediump float;
|
||||
uniform float uTime;
|
||||
uniform vec2 uResolution;
|
||||
void main() {
|
||||
vec2 uv=gl_FragCoord.xy/uResolution.xy;
|
||||
vec3 c1=vec3(0.1,0.4,0.8);
|
||||
vec3 c2=vec3(0.4,0.2,0.8);
|
||||
vec3 c3=vec3(0.1,0.6,0.4);
|
||||
float w1=sin(uv.x*3.0+uTime*0.5)*0.5+0.5;
|
||||
float w2=sin(uv.x*4.0+uTime*0.3+uv.y*2.0)*0.5+0.5;
|
||||
float w3=sin(uv.x*2.0+uTime*0.7+uv.y*3.0)*0.5+0.5;
|
||||
float d=uv.y*2.0-1.0;
|
||||
float g=exp(-d*d*2.0);
|
||||
vec3 a=(c1*w1+c2*w2+c3*w3)*g*0.6;
|
||||
vec3 b=mix(vec3(0.02,0.02,0.08),vec3(0.05,0.05,0.15),uv.y);
|
||||
gl_FragColor=vec4(b+a,1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
function loadShader(gl,type,source){
|
||||
const s=gl.createShader(type);
|
||||
gl.shaderSource(s,source);
|
||||
gl.compileShader(s);
|
||||
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){
|
||||
console.error('Shader error:',gl.getShaderInfoLog(s));
|
||||
gl.deleteShader(s);
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const vs=loadShader(gl,gl.VERTEX_SHADER,vsSource);
|
||||
const fs=loadShader(gl,gl.FRAGMENT_SHADER,fsSource);
|
||||
const prog=gl.createProgram();
|
||||
gl.attachShader(prog,vs);
|
||||
gl.attachShader(prog,fs);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){
|
||||
console.error('Program link error:',gl.getProgramInfoLog(prog));
|
||||
}
|
||||
|
||||
const posLoc=gl.getAttribLocation(prog,'aVertexPosition');
|
||||
const timeLoc=gl.getUniformLocation(prog,'uTime');
|
||||
const resLoc=gl.getUniformLocation(prog,'uResolution');
|
||||
|
||||
const posBuf=gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,1,1,1,-1,-1,1,-1]),gl.STATIC_DRAW);
|
||||
|
||||
let startTime=Date.now();
|
||||
const FPS = 15;
|
||||
let lastFrame = 0;
|
||||
|
||||
function render(ts){
|
||||
requestAnimationFrame(render);
|
||||
if (ts - lastFrame < 1000 / FPS) return;
|
||||
lastFrame = ts;
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
gl.clearColor(0,0,0,1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(prog);
|
||||
gl.enableVertexAttribArray(posLoc);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
|
||||
gl.vertexAttribPointer(posLoc,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform1f(timeLoc,(Date.now()-startTime)/1000);
|
||||
gl.uniform2f(resLoc,canvas.width,canvas.height);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
console.log('✅ WebGL 极光背景已启动');
|
||||
|
||||
// 时间和星座
|
||||
function getUserZodiac() {
|
||||
return window.userZodiac || '射手座';
|
||||
}
|
||||
|
||||
function updateZodiacDisplay() {
|
||||
var name = getUserZodiac();
|
||||
if (name === lastZodiac) return;
|
||||
lastZodiac = name;
|
||||
var zodiacEl = document.getElementById('zodiac');
|
||||
if (!zodiacEl) return;
|
||||
var fortune = fortunes[name] || '运势平稳,保持平常心。';
|
||||
zodiacEl.innerHTML = '✨ ' + name + '运势<br><small style="opacity:0.7">' + fortune + '</small>';
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
var now = new Date();
|
||||
var hh = String(now.getHours()).padStart(2, '0');
|
||||
var mm = String(now.getMinutes()).padStart(2, '0');
|
||||
var month = now.getMonth() + 1;
|
||||
var day = now.getDate();
|
||||
var week = ['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
|
||||
|
||||
var timeEl = document.getElementById('time');
|
||||
var dateEl = document.getElementById('date');
|
||||
|
||||
var timeStr = hh + ':' + mm;
|
||||
var dateStr = month + '月' + day + '日 ' + week;
|
||||
|
||||
if (timeEl && timeStr !== lastTimeStr) {
|
||||
timeEl.textContent = timeStr;
|
||||
lastTimeStr = timeStr;
|
||||
}
|
||||
if (dateEl && dateStr !== lastDateStr) {
|
||||
dateEl.textContent = dateStr;
|
||||
lastDateStr = dateStr;
|
||||
}
|
||||
|
||||
updateZodiacDisplay();
|
||||
}
|
||||
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
|
||||
console.log('=== 初始化完成 ===');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
win32.go
14
win32.go
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"unsafe"
|
||||
|
||||
@@ -9,9 +8,6 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//go:embed web/wallpaper.html
|
||||
var fs embed.FS
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
procFindWindowW = user32.NewProc("FindWindowW")
|
||||
@@ -37,6 +33,9 @@ var jsQueue = make(chan string, 64)
|
||||
var paused int32
|
||||
|
||||
const wmEvalJS = 0x0401
|
||||
const wmSetHtml = 0x0402
|
||||
|
||||
var htmlQueue = make(chan string, 1)
|
||||
|
||||
func evalJS(js string) {
|
||||
select {
|
||||
@@ -50,13 +49,6 @@ func evalJS(js string) {
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func findWorkerW() uintptr {
|
||||
progman, _, _ := procFindWindowW.Call(
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
|
||||
Reference in New Issue
Block a user