Some checks failed
Release / build (push) Failing after 2m53s
Add Syncthing installation detection (PATH + config file check) to both the tray app and setup installer. When missing, the tray shows an "Install Syncthing..." menu item and the setup opens the download page. Rewrite README with Mermaid topology graph, per-binary dependency tables, project layout, API endpoint reference, and shields.io badges. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
233 lines
5.5 KiB
Go
233 lines
5.5 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()
|
|
}
|
|
_ = config.Save(cfg)
|
|
}
|
|
|
|
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)
|
|
_ = config.Save(a.cfg)
|
|
log.Printf("re-discovered API key")
|
|
}
|