From d19041a248d28eee219469961060e6d58416b047 Mon Sep 17 00:00:00 2001 From: amemya Date: Fri, 26 Jun 2026 20:19:32 +0900 Subject: [PATCH 1/5] feat: add system tray resident mode support - Implemented system tray integration for background execution - Added 'ResidentMode' setting to toggle background behavior - Main window now hides on close when resident mode is enabled - Added 'Keep running in system tray' toggle in Preferences UI - Added basic tray icon for macOS --- build/trayicon.png | Bin 0 -> 831 bytes frontend/src/SettingsWindow.tsx | 15 +++++++++ main.go | 52 +++++++++++++++++++++++++++++++- settings.go | 4 +++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 build/trayicon.png diff --git a/build/trayicon.png b/build/trayicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5042d6f4e9cbf06069a47aa73be0b2e166f17b GIT binary patch literal 831 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDB+SC;hE;^%b*2hb1*Qr zXELw=S&Tp|1;h*t%nKM9n1M7SNNfQUTvluWGlC6LD5Gc`#lXN+<>}%WVj*}n#5Y?i zP{dZsK1pQu5w5_6QsRpgJRLe`KRC96B~#p~&fyR1U)H;kT(Q6Nmn`U*6gp|0k3iM| zj%At~AGtri{$*cuT7WT^>g;a^&s2XuGxzhkz2EP$|H*e=tQ5f;Tv*NKxw*rssI)Y- zZ1>&yyll=9ivu4--kIQH2nQrE5gNUGTV1YjGoKlz+acl`)$9`!bK5mM{+hsY}mN* zVAh*&*LUpL;jsE@Pyv_Z_3NQCeAHN)9bJ?r-n?JAwzf@R!@hm%4y0{n{aGUyB6DoT zjyY|=f4>JR@=%*BaN|*gmyTas*y^K~FAIAu73y(Yd_pn#Y0*scMI{fPJV}{2`R&vr zPM&MS4xc^C`|aB|g^sGda_7#UxAbKaJ{l^$>WI_afYp~@2CNJ*kl_>TKYnxF_1Al5 zDhOv?zjp0P#X>KgBAt%qmo-RD4{h~fIKG0|CKd;*O#U?uIJN{fhw# literal 0 HcmV?d00001 diff --git a/frontend/src/SettingsWindow.tsx b/frontend/src/SettingsWindow.tsx index 02ddd47..e87c02b 100644 --- a/frontend/src/SettingsWindow.tsx +++ b/frontend/src/SettingsWindow.tsx @@ -19,6 +19,7 @@ function SettingsWindow() { const [exportFolder, setExportFolder] = useState(""); const [jpegQuality, setJpegQuality] = useState("auto"); const [enableBetaUpdates, setEnableBetaUpdates] = useState(false); + const [residentMode, setResidentMode] = useState(true); // Frame Settings const [aspectRatioPreset, setAspectRatioPreset] = useState("4300:3618"); @@ -48,6 +49,7 @@ function SettingsWindow() { setExportFolder(s.exportFolder || ""); setJpegQuality(s.jpegQuality || "auto"); setEnableBetaUpdates(s.enableBetaUpdates ?? false); + setResidentMode(s.residentMode ?? true); setAspectRatioPreset(s.aspectRatioPreset || "4300:3618"); setCustomRatioW(s.customRatioW || 4300); @@ -105,6 +107,7 @@ function SettingsWindow() { s.exportFolder = exportFolder; s.jpegQuality = jpegQuality; s.enableBetaUpdates = enableBetaUpdates; + s.residentMode = residentMode; s.aspectRatioPreset = aspectRatioPreset; s.customRatioW = customRatioW; s.customRatioH = customRatioH; @@ -237,6 +240,18 @@ function SettingsWindow() { Receive pre-release (beta) versions of ExifFrame automatically. +
+ + When enabled, closing the window keeps ExifFrame running in the menu bar. Changes take effect after restarting the app. +
)} {activeTab === 'frame' && ( diff --git a/main.go b/main.go index 4cd40c9..d84a278 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,9 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" ) +//go:embed build/trayicon.png +var trayIcon []byte + //go:embed all:frontend/dist var assets embed.FS @@ -88,7 +91,7 @@ func main() { Middleware: handler.Middleware, }, Mac: application.MacOptions{ - ApplicationShouldTerminateAfterLastWindowClosed: true, + ApplicationShouldTerminateAfterLastWindowClosed: false, }, }) app.Menu.SetApplicationMenu(buildMenu(appStruct)) @@ -96,6 +99,11 @@ func main() { // Initialise the in-app updater (GitHub-backed, with periodic checks). InitUpdater(app) + // Read resident mode setting (read once at startup; changes require restart). + settingsMu.RLock() + residentMode := currentSettings.ResidentMode + settingsMu.RUnlock() + win := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "ExifFrame", Width: 1024, @@ -119,6 +127,48 @@ func main() { } }) + // --- System Tray (Resident Mode) --- + if residentMode { + // Intercept window close: hide instead of destroy. + win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + win.Hide() + e.Cancel() + }) + + // Build tray right-click menu. + trayMenu := application.NewMenu() + trayMenu.Add("Show ExifFrame").OnClick(func(ctx *application.Context) { + win.Show() + win.Focus() + }) + trayMenu.Add("Preferences...").OnClick(func(ctx *application.Context) { + appStruct.OpenSettingsWindow() + }) + trayMenu.AddSeparator() + trayMenu.Add("Quit ExifFrame").OnClick(func(ctx *application.Context) { + application.Get().Quit() + }) + + systray := app.SystemTray.New() + if runtime.GOOS == "darwin" { + systray.SetTemplateIcon(trayIcon) // Auto-adapts to dark/light mode on macOS + } else { + systray.SetIcon(trayIcon) + } + systray.SetMenu(trayMenu) + systray.SetTooltip("ExifFrame") + + // Left-click toggles main window visibility. + systray.OnClick(func() { + if win.IsVisible() { + win.Hide() + } else { + win.Show() + win.Focus() + } + }) + } + err := app.Run() if err != nil { log.Fatal("Error:", err) diff --git a/settings.go b/settings.go index 2bfe1c7..e916ad1 100644 --- a/settings.go +++ b/settings.go @@ -55,6 +55,9 @@ type Settings struct { // Updater settings EnableBetaUpdates bool `json:"enableBetaUpdates"` + + // System tray settings + ResidentMode bool `json:"residentMode"` } var ( @@ -101,6 +104,7 @@ func init() { VisibilityTemperature: true, VisibilityTime: true, EnableBetaUpdates: false, + ResidentMode: true, } loadSettings() } From 894dea86721d8ee5bf3ab26ded64cfc96785f4c8 Mon Sep 17 00:00:00 2001 From: amemya Date: Fri, 26 Jun 2026 20:25:30 +0900 Subject: [PATCH 2/5] fix: dynamically set macOS application termination based on residentMode Ensures the app will properly exit when the last window is closed if resident mode is disabled. --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index d84a278..38bb77b 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,11 @@ func main() { handler := NewImageHandler(appStruct) appStruct.handler = handler + // Read resident mode setting (read once at startup; changes require restart). + settingsMu.RLock() + residentMode := currentSettings.ResidentMode + settingsMu.RUnlock() + app := application.New(application.Options{ Name: "ExifFrame", Description: "ExifFrame", @@ -91,7 +96,7 @@ func main() { Middleware: handler.Middleware, }, Mac: application.MacOptions{ - ApplicationShouldTerminateAfterLastWindowClosed: false, + ApplicationShouldTerminateAfterLastWindowClosed: !residentMode, }, }) app.Menu.SetApplicationMenu(buildMenu(appStruct)) @@ -99,11 +104,6 @@ func main() { // Initialise the in-app updater (GitHub-backed, with periodic checks). InitUpdater(app) - // Read resident mode setting (read once at startup; changes require restart). - settingsMu.RLock() - residentMode := currentSettings.ResidentMode - settingsMu.RUnlock() - win := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "ExifFrame", Width: 1024, From 169c6228136674316480541668a0bc712f2711d5 Mon Sep 17 00:00:00 2001 From: amemya Date: Fri, 26 Jun 2026 20:32:21 +0900 Subject: [PATCH 3/5] fix: ensure application can quit when resident mode is enabled Added an atomic isQuitting flag that bypasses the WindowClosing cancel hook, allowing the application to gracefully close windows and terminate when Quit is triggered from the menu or system tray. --- main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.go b/main.go index 38bb77b..75dd180 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "embed" "log" "runtime" + "sync/atomic" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" @@ -15,6 +16,8 @@ var trayIcon []byte //go:embed all:frontend/dist var assets embed.FS +var isQuitting atomic.Bool + func buildMenu(app *App) *application.Menu { appMenu := application.NewMenu() @@ -33,6 +36,7 @@ func buildMenu(app *App) *application.Menu { appleMenu.AddRole(application.ShowAll) appleMenu.AddSeparator() appleMenu.Add("Quit ExifFrame").SetAccelerator("CmdOrCtrl+q").OnClick(func(ctx *application.Context) { + isQuitting.Store(true) application.Get().Quit() }) @@ -131,6 +135,9 @@ func main() { if residentMode { // Intercept window close: hide instead of destroy. win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + if isQuitting.Load() { + return + } win.Hide() e.Cancel() }) @@ -146,6 +153,7 @@ func main() { }) trayMenu.AddSeparator() trayMenu.Add("Quit ExifFrame").OnClick(func(ctx *application.Context) { + isQuitting.Store(true) application.Get().Quit() }) From 4a6ba7f44e5a014bd0ccf8f9bd29fc7aff120d95 Mon Sep 17 00:00:00 2001 From: amemya Date: Sat, 27 Jun 2026 00:07:53 +0900 Subject: [PATCH 4/5] fix: centralize isQuitting flag inside ShouldQuit callback Ensures that the quitting flag is set on all application quit routes (including Cmd+Q, Dock quit, and OS shutdown) so that the WindowClosing hook can properly allow windows to close and the app to terminate. --- main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 75dd180..247356a 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,6 @@ func buildMenu(app *App) *application.Menu { appleMenu.AddRole(application.ShowAll) appleMenu.AddSeparator() appleMenu.Add("Quit ExifFrame").SetAccelerator("CmdOrCtrl+q").OnClick(func(ctx *application.Context) { - isQuitting.Store(true) application.Get().Quit() }) @@ -102,6 +101,10 @@ func main() { Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: !residentMode, }, + ShouldQuit: func() bool { + isQuitting.Store(true) + return true + }, }) app.Menu.SetApplicationMenu(buildMenu(appStruct)) @@ -153,7 +156,6 @@ func main() { }) trayMenu.AddSeparator() trayMenu.Add("Quit ExifFrame").OnClick(func(ctx *application.Context) { - isQuitting.Store(true) application.Get().Quit() }) From 7c65cf30a7832bed3883edd5ff601d151987384a Mon Sep 17 00:00:00 2001 From: amemya Date: Sat, 27 Jun 2026 00:40:17 +0900 Subject: [PATCH 5/5] fix: keep frontend/dist directory to prevent go:embed errors on fresh clone --- .gitignore | 3 ++- frontend/dist/.gitkeep | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/dist/.gitkeep diff --git a/.gitignore b/.gitignore index 94a26b5..03ec120 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/bin node_modules -frontend/dist +frontend/dist/* +!frontend/dist/.gitkeep frontend/coverage # macOS .DS_Store diff --git a/frontend/dist/.gitkeep b/frontend/dist/.gitkeep new file mode 100644 index 0000000..e69de29