Initial GoWallpaper implementation
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
//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
|
||||
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.dll", 4)
|
||||
if err != nil {
|
||||
ni.Dispose()
|
||||
return nil, fmt.Errorf("加载托盘图标: %w", err)
|
||||
}
|
||||
if err := ni.SetIcon(icon); err != nil {
|
||||
ni.Dispose()
|
||||
return nil, 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())
|
||||
} else {
|
||||
_ = a.notifyIcon.ShowInfo(appName, "壁纸已刷新。")
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user