Reuse a single long-poll HTTP client instead of creating one per Events() call (~every 30s). Make TLS skip-verify configurable via syncthing_insecure_tls. Log previously swallowed config errors. Add unit tests for all monitor trackers, config, and state logic. Add CI workflow (vet, golangci-lint, govulncheck, go test -race). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
237 lines
5.7 KiB
Go
237 lines
5.7 KiB
Go
package tray
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/energye/systray"
|
|
|
|
"git.davoryn.de/calic/syncwarden/internal/config"
|
|
st "git.davoryn.de/calic/syncwarden/internal/syncthing"
|
|
)
|
|
|
|
// buildMenu creates the full context menu.
|
|
func (a *App) buildMenu() {
|
|
// Status info section
|
|
a.statusItem = systray.AddMenuItem("Status: Connecting...", "")
|
|
a.statusItem.Disable()
|
|
|
|
a.rateItem = systray.AddMenuItem("↓ 0 B/s ↑ 0 B/s", "")
|
|
a.rateItem.Disable()
|
|
|
|
a.devicesItem = systray.AddMenuItem("Devices: —", "")
|
|
a.devicesItem.Disable()
|
|
|
|
a.lastSyncItem = systray.AddMenuItem("Last sync: —", "")
|
|
a.lastSyncItem.Disable()
|
|
|
|
if a.syncthingMissing {
|
|
mInstall := systray.AddMenuItem("Install Syncthing...", "Download Syncthing")
|
|
mInstall.Click(func() {
|
|
openBrowser(st.DownloadURL)
|
|
})
|
|
}
|
|
|
|
systray.AddSeparator()
|
|
|
|
// Open Admin Panel
|
|
mOpenPanel := systray.AddMenuItem("Open Admin Panel", "Open Syncthing admin panel")
|
|
mOpenPanel.Click(func() {
|
|
a.openPanel()
|
|
})
|
|
|
|
// Pause/Resume toggle
|
|
a.pauseItem = systray.AddMenuItem("Pause All", "Pause/Resume all syncing")
|
|
a.pauseItem.Click(func() {
|
|
a.togglePause()
|
|
})
|
|
|
|
systray.AddSeparator()
|
|
|
|
// Folders submenu
|
|
a.foldersMenu = systray.AddMenuItem("Folders", "")
|
|
emptyFolder := a.foldersMenu.AddSubMenuItem("(loading...)", "")
|
|
emptyFolder.Disable()
|
|
a.folderItems = []*systray.MenuItem{emptyFolder}
|
|
|
|
// Recent Files submenu
|
|
a.recentMenu = systray.AddMenuItem("Recent Files", "")
|
|
emptyRecent := a.recentMenu.AddSubMenuItem("(none)", "")
|
|
emptyRecent.Disable()
|
|
a.recentItems = []*systray.MenuItem{emptyRecent}
|
|
|
|
// Conflicts
|
|
a.conflictItem = systray.AddMenuItem("Conflicts (0)", "Open conflicts page")
|
|
a.conflictItem.Hide()
|
|
a.conflictItem.Click(func() {
|
|
a.openConflicts()
|
|
})
|
|
|
|
systray.AddSeparator()
|
|
|
|
// Actions
|
|
mRescan := systray.AddMenuItem("Rescan All", "Trigger rescan of all folders")
|
|
mRescan.Click(func() {
|
|
go func() {
|
|
if err := a.client.RescanAll(); err != nil {
|
|
log.Printf("rescan error: %v", err)
|
|
}
|
|
}()
|
|
})
|
|
|
|
mRestart := systray.AddMenuItem("Restart Syncthing", "Restart the Syncthing process")
|
|
mRestart.Click(func() {
|
|
go func() {
|
|
if err := a.client.Restart(); err != nil {
|
|
log.Printf("restart error: %v", err)
|
|
}
|
|
}()
|
|
})
|
|
|
|
systray.AddSeparator()
|
|
|
|
// Settings submenu
|
|
mSettings := systray.AddMenuItem("Settings", "")
|
|
|
|
chkNotify := mSettings.AddSubMenuItem("Notifications", "Enable/disable notifications")
|
|
if a.cfg.EnableNotifications {
|
|
chkNotify.Check()
|
|
}
|
|
chkNotify.Click(func() {
|
|
a.toggleSetting(&a.cfg.EnableNotifications, chkNotify)
|
|
})
|
|
|
|
chkRecent := mSettings.AddSubMenuItem("Show Recent Files", "Show recently synced files")
|
|
if a.cfg.EnableRecentFiles {
|
|
chkRecent.Check()
|
|
}
|
|
chkRecent.Click(func() {
|
|
a.toggleSetting(&a.cfg.EnableRecentFiles, chkRecent)
|
|
})
|
|
|
|
chkConflict := mSettings.AddSubMenuItem("Conflict Alerts", "Alert on sync conflicts")
|
|
if a.cfg.EnableConflictAlerts {
|
|
chkConflict.Check()
|
|
}
|
|
chkConflict.Click(func() {
|
|
a.toggleSetting(&a.cfg.EnableConflictAlerts, chkConflict)
|
|
})
|
|
|
|
chkRate := mSettings.AddSubMenuItem("Transfer Rate in Tooltip", "Show transfer rate in tooltip")
|
|
if a.cfg.EnableTransferRate {
|
|
chkRate.Check()
|
|
}
|
|
chkRate.Click(func() {
|
|
a.toggleSetting(&a.cfg.EnableTransferRate, chkRate)
|
|
})
|
|
|
|
chkAutoStart := mSettings.AddSubMenuItem("Auto-start Syncthing", "Start Syncthing when SyncWarden starts")
|
|
if a.cfg.AutoStartSyncthing {
|
|
chkAutoStart.Check()
|
|
}
|
|
chkAutoStart.Click(func() {
|
|
a.toggleSetting(&a.cfg.AutoStartSyncthing, chkAutoStart)
|
|
})
|
|
|
|
chkLogin := mSettings.AddSubMenuItem("Start on Login", "Start SyncWarden at system login")
|
|
if a.cfg.StartOnLogin {
|
|
chkLogin.Check()
|
|
}
|
|
chkLogin.Click(func() {
|
|
a.toggleSetting(&a.cfg.StartOnLogin, chkLogin)
|
|
})
|
|
|
|
// API key info
|
|
apiKeyDisplay := "API Key: (none)"
|
|
if len(a.cfg.SyncthingAPIKey) > 8 {
|
|
apiKeyDisplay = fmt.Sprintf("API Key: %s...%s", a.cfg.SyncthingAPIKey[:4], a.cfg.SyncthingAPIKey[len(a.cfg.SyncthingAPIKey)-4:])
|
|
} else if a.cfg.SyncthingAPIKey != "" {
|
|
apiKeyDisplay = fmt.Sprintf("API Key: %s", a.cfg.SyncthingAPIKey)
|
|
}
|
|
mAPIKey := mSettings.AddSubMenuItem(apiKeyDisplay, "")
|
|
mAPIKey.Disable()
|
|
|
|
mRediscover := mSettings.AddSubMenuItem("Re-discover API Key", "Re-read API key from Syncthing config")
|
|
mRediscover.Click(func() {
|
|
go a.rediscoverAPIKey()
|
|
})
|
|
|
|
mAddr := mSettings.AddSubMenuItem(fmt.Sprintf("Address: %s", a.cfg.SyncthingAddress), "")
|
|
mAddr.Disable()
|
|
|
|
// About
|
|
mAbout := systray.AddMenuItem(fmt.Sprintf("About (v%s)", version), "")
|
|
mAbout.Disable()
|
|
|
|
systray.AddSeparator()
|
|
|
|
// Quit
|
|
mQuit := systray.AddMenuItem("Quit", "Exit SyncWarden")
|
|
mQuit.Click(func() {
|
|
log.Println("Quit clicked")
|
|
systray.Quit()
|
|
})
|
|
}
|
|
|
|
func (a *App) togglePause() {
|
|
a.mu.Lock()
|
|
paused := a.lastStatus.Paused
|
|
a.mu.Unlock()
|
|
|
|
go func() {
|
|
var err error
|
|
if paused {
|
|
err = a.client.ResumeAll()
|
|
} else {
|
|
err = a.client.PauseAll()
|
|
}
|
|
if err != nil {
|
|
log.Printf("pause/resume error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *App) openConflicts() {
|
|
// Open the Syncthing conflicts page in the panel
|
|
launchPanel(a.cfg.BaseURL())
|
|
}
|
|
|
|
func (a *App) toggleSetting(field *bool, item *systray.MenuItem) {
|
|
a.mu.Lock()
|
|
*field = !*field
|
|
val := *field
|
|
cfg := a.cfg
|
|
a.mu.Unlock()
|
|
|
|
if val {
|
|
item.Check()
|
|
} else {
|
|
item.Uncheck()
|
|
}
|
|
if err := config.Save(cfg); err != nil {
|
|
log.Printf("config save error: %v", err)
|
|
}
|
|
}
|
|
|
|
func (a *App) rediscoverAPIKey() {
|
|
key, err := st.DiscoverAPIKey()
|
|
if err != nil {
|
|
log.Printf("API key discovery failed: %v", err)
|
|
return
|
|
}
|
|
if key == "" {
|
|
log.Println("no API key found in Syncthing config")
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.cfg.SyncthingAPIKey = key
|
|
a.mu.Unlock()
|
|
|
|
a.client.SetAPIKey(key)
|
|
if err := config.Save(a.cfg); err != nil {
|
|
log.Printf("config save error: %v", err)
|
|
}
|
|
log.Printf("re-discovered API key")
|
|
}
|