//go:build windows package main import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "sync" "sync/atomic" "time" "github.com/lxn/walk" ) const appName = "GoWallpaper" type App struct { mw *walk.MainWindow notifyIcon *walk.NotifyIcon configPath string cacheDir string config Config cache *CacheIndex downloader *Downloader autoAction *walk.Action refreshAction *walk.Action previousAction *walk.Action nextAction *walk.Action settingsAction *walk.Action settingsDialog *walk.Dialog timerMu sync.Mutex timer *time.Timer busy atomic.Bool closing atomic.Bool } func newApp(mw *walk.MainWindow, appDir string, cfg Config, cache *CacheIndex) (*App, error) { ni, err := walk.NewNotifyIcon(mw) if err != nil { return nil, err } icon, err := walk.NewIconFromSysDLL("shell32", 4) if err != nil { icon = walk.IconApplication() } if err := ni.SetIcon(icon); err != nil { ni.Dispose() return nil, fmt.Errorf("设置托盘图标: %w", err) } if err := ni.SetToolTip(appName + " - 右键打开菜单"); err != nil { ni.Dispose() return nil, err } a := &App{ mw: mw, notifyIcon: ni, configPath: filepath.Join(appDir, configFileName), cacheDir: filepath.Join(appDir, "cache"), config: cfg, cache: cache, downloader: newDownloader(), } if err := a.createMenu(); err != nil { ni.Dispose() return nil, err } if err := ni.SetVisible(true); err != nil { ni.Dispose() return nil, err } return a, nil } func (a *App) createMenu() error { var err error if a.refreshAction, err = a.addAction("刷新壁纸", func() { a.refresh(false) }); err != nil { return err } if a.previousAction, err = a.addAction("上一张", func() { a.moveHistory(-1) }); err != nil { return err } if a.nextAction, err = a.addAction("下一张", func() { a.moveHistory(1) }); err != nil { return err } if err := a.notifyIcon.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil { return err } if a.autoAction, err = a.addAction("自动切换壁纸", a.toggleAutoChange); err != nil { return err } if err := a.autoAction.SetCheckable(true); err != nil { return err } if err := a.autoAction.SetChecked(a.config.AutoChangeEnabled); err != nil { return err } if a.settingsAction, err = a.addAction("设置...", a.showSettings); err != nil { return err } if _, err = a.addAction("打开缓存目录", a.openCacheDirectory); err != nil { return err } if err := a.notifyIcon.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil { return err } if _, err = a.addAction("关于", func() { walk.MsgBox(a.mw, "关于 GoWallpaper", "GoWallpaper\n\n通过随机图片 HTTP API 手动或定时更换 Windows 壁纸。", walk.MsgBoxIconInformation) }); err != nil { return err } _, err = a.addAction("退出", a.close) return err } func (a *App) addAction(text string, handler func()) (*walk.Action, error) { action := walk.NewAction() if err := action.SetText(text); err != nil { return nil, err } action.Triggered().Attach(handler) if err := a.notifyIcon.ContextMenu().Actions().Add(action); err != nil { return nil, err } return action, nil } func (a *App) start(firstRun bool) { a.scheduleTimer() _ = a.notifyIcon.ShowInfo(appName, "软件已经运行,可右键托盘图标进行操作。") if firstRun || a.config.APIURL == "" { time.AfterFunc(300*time.Millisecond, func() { a.mw.Synchronize(func() { a.showSettings() }) }) } } func (a *App) close() { if !a.closing.CompareAndSwap(false, true) { return } a.stopTimer() a.notifyIcon.SetVisible(false) a.notifyIcon.Dispose() walk.App().Exit(0) } func (a *App) setBusy(busy bool) { a.refreshAction.SetEnabled(!busy) a.previousAction.SetEnabled(!busy) a.nextAction.SetEnabled(!busy) a.settingsAction.SetEnabled(!busy) } func (a *App) refresh(automatic bool) { if err := a.config.validate(true); err != nil { if !automatic { a.notifyWarning(err.Error()) a.showSettings() } else { a.scheduleTimer() } return } if !a.busy.CompareAndSwap(false, true) { if !automatic { a.notifyWarning("正在刷新壁纸,请稍候。") } else { a.scheduleTimer() } return } a.setBusy(true) cfg := a.config go func() { path := uniqueCachePath(a.cacheDir) wallpaperSet := false err := a.downloader.download(context.Background(), cfg.APIURL, path) if err == nil { err = setWallpaper(path, cfg.WallpaperStyle) wallpaperSet = err == nil } if err == nil { err = a.cache.add(path, cfg.MaxCacheCount) } if err != nil && !wallpaperSet { _ = os.Remove(path) } a.mw.Synchronize(func() { a.busy.Store(false) a.setBusy(false) if err != nil { a.notifyError("刷新壁纸失败:" + err.Error()) } if automatic { a.scheduleTimer() } }) }() } func (a *App) moveHistory(direction int) { oldCurrent := a.cache.Current var path string var moved bool var err error if direction < 0 { path, moved, err = a.cache.previous() } else { path, moved, err = a.cache.next() } if err != nil { a.notifyError(err.Error()) return } if !moved { if direction < 0 { a.notifyWarning("已经是缓存中最旧的壁纸。") } else { a.notifyWarning("已经是缓存中最新的壁纸。") } return } if err := setWallpaper(path, a.config.WallpaperStyle); err != nil { a.cache.Current = oldCurrent _ = a.cache.save() a.notifyError("切换壁纸失败:" + err.Error()) return } } func (a *App) toggleAutoChange() { old := a.config.AutoChangeEnabled a.config.AutoChangeEnabled = !old if err := saveConfig(a.configPath, a.config); err != nil { a.config.AutoChangeEnabled = old _ = a.autoAction.SetChecked(old) a.notifyError(err.Error()) return } _ = a.autoAction.SetChecked(a.config.AutoChangeEnabled) a.scheduleTimer() if a.config.AutoChangeEnabled { a.notifyInfo(fmt.Sprintf("自动切换已开启,每 %d 分钟刷新一次。", a.config.IntervalMinutes)) } else { a.notifyInfo("自动切换已关闭。") } } func (a *App) scheduleTimer() { a.timerMu.Lock() defer a.timerMu.Unlock() if a.timer != nil { a.timer.Stop() a.timer = nil } if a.config.AutoChangeEnabled && !a.closing.Load() { duration := time.Duration(a.config.IntervalMinutes) * time.Minute a.timer = time.AfterFunc(duration, func() { if !a.closing.Load() { a.mw.Synchronize(func() { a.refresh(true) }) } }) } } func (a *App) stopTimer() { a.timerMu.Lock() defer a.timerMu.Unlock() if a.timer != nil { a.timer.Stop() a.timer = nil } } func (a *App) openCacheDirectory() { if err := exec.Command("explorer.exe", a.cacheDir).Start(); err != nil { a.notifyError("无法打开缓存目录:" + err.Error()) } } func (a *App) applyConfig(cfg Config) error { if err := cfg.validate(false); err != nil { return err } old := a.config if err := setStartWithWindows(cfg.StartWithWindows); err != nil { return err } if err := saveConfig(a.configPath, cfg); err != nil { _ = setStartWithWindows(old.StartWithWindows) return err } a.config = cfg if err := a.cache.trim(cfg.MaxCacheCount); err != nil { a.config = old _ = saveConfig(a.configPath, old) _ = setStartWithWindows(old.StartWithWindows) return err } _ = a.autoAction.SetChecked(cfg.AutoChangeEnabled) a.scheduleTimer() return nil } func (a *App) notifyInfo(message string) { _ = a.notifyIcon.ShowInfo(appName, message) } func (a *App) notifyWarning(message string) { _ = a.notifyIcon.ShowWarning(appName, message) } func (a *App) notifyError(message string) { _ = a.notifyIcon.ShowError(appName, message) } func appDirectory() (string, error) { exe, err := os.Executable() if err != nil { return "", err } dir := filepath.Dir(exe) probe, err := os.CreateTemp(dir, ".gowallpaper-write-test-*") if err != nil { return "", fmt.Errorf("程序目录不可写:%w", err) } name := probe.Name() probe.Close() if err := os.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) { return "", err } return dir, nil }