Files
gowallpaper/app.go
T

340 lines
8.0 KiB
Go

//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
}
_ = a.notifyIcon.ShowInfo(appName, "已切换到缓存壁纸。")
}
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
}