From 8e14bc68237dd7bfb734024f02b109b7dd73a214 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Mon, 19 Jan 2026 23:56:58 -0500 Subject: [PATCH 1/6] Documentation note about deleted items for #91 --- docs/USER_GUIDE.md | 7 +++++++ go.mod | 14 +++++++------- go.sum | 12 ++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f8b6de0..5aad822 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -153,6 +153,13 @@ You can change these mappings later from [Settings](SETTINGS.md). ## Background Cache Sync +> [!INFO] +> Grout currently does not gracefully handle deletions. +> +> Deleted games, platforms and collections will continue to be shown until the [local cache is rebuilt](SETTINGS.md#rebuild-cache). +> +> We are waiting for updated API endpoints that will allow Grout to remove deleted items from the cache without a rebuild. You can track the progress of this in [this issue](https://github.com/rommapp/grout/issues/83). + Grout maintains a local cache of your RomM library data (platforms, games, and collections) to provide a fast, responsive browsing experience. This cache syncs automatically in the background each time you launch Grout. diff --git a/go.mod b/go.mod index 72be02d..24c600e 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.24.2 require ( github.com/BrandonKowalski/certifiable v1.3.0 - github.com/BrandonKowalski/gabagool/v2 v2.6.3 + github.com/BrandonKowalski/gabagool/v2 v2.6.4 + github.com/beevik/etree v1.6.0 github.com/bodgit/sevenzip v1.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/piglig/go-qr v0.2.6 @@ -12,23 +13,22 @@ require ( go.uber.org/atomic v1.11.0 golang.org/x/image v0.35.0 golang.org/x/text v0.33.0 - modernc.org/sqlite v1.43.0 + modernc.org/sqlite v1.44.2 ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect - github.com/beevik/etree v1.6.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 // indirect - github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pierrec/lz4/v4 v4.1.23 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/afero v1.15.0 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect @@ -37,11 +37,11 @@ require ( github.com/ulikunitz/xz v0.5.15 // indirect github.com/veandco/go-sdl2 v0.4.40 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect - modernc.org/libc v1.67.4 // indirect + modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 3464660..edf27df 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/BrandonKowalski/certifiable v1.3.0 h1:o4VbMMVqBQ8kYFqUFD7J3P6tJg9dVKW github.com/BrandonKowalski/certifiable v1.3.0/go.mod h1:fnYhDBTxp3ZeTYFkWDXSAo5m8ytUwGG/3dZHSzDH9UU= github.com/BrandonKowalski/gabagool/v2 v2.6.3 h1:eYnPdDAzMU4bijIWdFUEjFNbGoQV5+MsrUG45RpaufQ= github.com/BrandonKowalski/gabagool/v2 v2.6.3/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= +github.com/BrandonKowalski/gabagool/v2 v2.6.4 h1:yMvcDFgNnIBpuKnLuAllZpNX0dMYijybgPMDsDCv50U= +github.com/BrandonKowalski/gabagool/v2 v2.6.4/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -88,6 +90,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -101,6 +105,8 @@ github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU= github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/piglig/go-qr v0.2.6 h1:oE8v9NGXHWahoFrnS44xsq0Lq0oIRIV7gMBM8cfRGwE= github.com/piglig/go-qr v0.2.6/go.mod h1:funyXL4IdgMPcbICoVm1XweMtZy7Px3kyITTENkmA5w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -145,6 +151,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= +go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -322,6 +330,8 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg= modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -332,6 +342,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA= modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= +modernc.org/sqlite v1.44.2 h1:EdYqXeBpKFJjg8QYnw6E71MpANkoxyuYi+g68ugOL8g= +modernc.org/sqlite v1.44.2/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From c8b157c0ee385eaf16337b38f1a5f7cb0b3be157 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Mon, 19 Jan 2026 23:58:04 -0500 Subject: [PATCH 2/6] INFO isn't an alert name apparently --- docs/USER_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 5aad822..2824574 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -153,7 +153,7 @@ You can change these mappings later from [Settings](SETTINGS.md). ## Background Cache Sync -> [!INFO] +> [!WARNING] > Grout currently does not gracefully handle deletions. > > Deleted games, platforms and collections will continue to be shown until the [local cache is rebuilt](SETTINGS.md#rebuild-cache). From 0701e151a6e683711fc2a876d09b5bf1892904a5 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Wed, 21 Jan 2026 00:00:31 -0500 Subject: [PATCH 3/6] Gabagool refactor, components now use standardized language. FSM replaced with Router for less contexty global state. Should be easier to reason about. --- app/grout.go | 10 +- app/helpers.go | 169 +++ app/router.go | 199 ++++ app/screens.go | 31 + app/setup.go | 6 +- app/state.go | 42 + app/states.go | 994 ------------------ app/transitions.go | 585 +++++++++++ cfw/muos/input_mapping.go | 3 - go.mod | 4 +- go.sum | 4 + internal/config.go | 131 +-- internal/constants.go | 15 + internal/constants/exit_codes.go | 29 - internal/constants/icons.go | 4 - internal/constants/timeouts.go | 10 - internal/enums.go | 40 + .../{variables.go => environment.go} | 0 internal/jsonutil/{loader.go => jsonutil.go} | 2 - internal/platforms.go | 102 ++ ui/actions.go | 154 +++ ui/advanced_settings.go | 31 +- ui/artwork_sync.go | 2 +- ui/bios_download.go | 22 +- ui/collection_platform_selection.go | 24 +- ui/collection_selection.go | 22 +- ui/collections_settings.go | 22 +- ui/download.go | 18 +- ui/emulator_selection.go | 12 +- ui/game_details.go | 21 +- ui/game_options.go | 18 +- ui/games_list.go | 69 +- ui/general_settings.go | 32 +- ui/info.go | 18 +- ui/login.go | 20 +- ui/logout_confirmation.go | 19 +- ui/platform_selection.go | 27 +- ui/rebuild_cache.go | 85 ++ ui/save_sync.go | 4 +- ui/save_sync_settings.go | 18 +- ui/screen_result.go | 29 - ui/search.go | 11 +- ui/settings.go | 70 +- ui/settings_platform_mapping.go | 18 +- ui/sync_report.go | 10 +- ui/update.go | 17 +- update/updater.go | 3 +- 47 files changed, 1728 insertions(+), 1448 deletions(-) create mode 100644 app/helpers.go create mode 100644 app/router.go create mode 100644 app/screens.go create mode 100644 app/state.go delete mode 100644 app/states.go create mode 100644 app/transitions.go create mode 100644 internal/constants.go delete mode 100644 internal/constants/exit_codes.go delete mode 100644 internal/constants/icons.go delete mode 100644 internal/constants/timeouts.go create mode 100644 internal/enums.go rename internal/environment/{variables.go => environment.go} (100%) rename internal/jsonutil/{loader.go => jsonutil.go} (90%) create mode 100644 internal/platforms.go create mode 100644 ui/actions.go create mode 100644 ui/rebuild_cache.go delete mode 100644 ui/screen_result.go diff --git a/app/grout.go b/app/grout.go index 73e2cc7..fa96d45 100644 --- a/app/grout.go +++ b/app/grout.go @@ -24,21 +24,19 @@ func main() { quitOnBack := len(config.Hosts) == 1 showCollections := config.ShowCollections(config.Hosts[0]) - fsm := buildFSM(config, currentCFW, platforms, quitOnBack, showCollections) - - if err := fsm.Run(); err != nil { - logger.Error("FSM error", "error", err) + if err := runWithRouter(config, currentCFW, platforms, quitOnBack, showCollections); err != nil { + logger.Error("Router error", "error", err) } } func cleanup() { - if autoSync != nil && autoSync.IsRunning() { + if currentAppState != nil && currentAppState.AutoSync != nil && currentAppState.AutoSync.IsRunning() { gaba.GetLogger().Info("Waiting for auto-sync to complete before exiting...") gaba.ProcessMessage( i18n.Localize(&goi18n.Message{ID: "auto_sync_waiting", Other: "Waiting for save sync to complete..."}, nil), gaba.ProcessMessageOptions{}, func() (interface{}, error) { - autoSync.Wait() + currentAppState.AutoSync.Wait() return nil, nil }, ) diff --git a/app/helpers.go b/app/helpers.go new file mode 100644 index 0000000..1ecc11d --- /dev/null +++ b/app/helpers.go @@ -0,0 +1,169 @@ +package main + +import ( + "grout/cache" + "grout/cfw" + "grout/internal" + "grout/romm" + "grout/ui" + + gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/router" + goi18n "github.com/nicksnyder/go-i18n/v2/i18n" + uatomic "go.uber.org/atomic" +) + +func filterGamesByPlatform(games []romm.Rom, platformID int) []romm.Rom { + filtered := make([]romm.Rom, 0) + for _, game := range games { + if game.PlatformID == platformID { + filtered = append(filtered, game) + } + } + return filtered +} + +func savePlatformOrder(state *AppState, platforms []romm.Platform) { + var platformOrder []string + for _, p := range platforms { + platformOrder = append(platformOrder, p.Slug) + } + state.Config.PlatformOrder = platformOrder + state.Platforms = platforms + internal.SaveConfig(state.Config) +} + +func triggerAutoSyncRouter(state *AppState) { + if state.AutoSync != nil { + state.AutoSync.Trigger() + } +} + +func executeDownloadUI(state *AppState, r ui.GameDetailsOutput, stack *router.Stack) { + entry := stack.Peek() + var allGames []romm.Rom + var searchFilter string + if entry != nil { + if input, ok := entry.Input.(ui.GameListInput); ok { + allGames = input.Games + searchFilter = input.SearchFilter + } + } + + downloadScreen := ui.NewDownloadScreen() + downloadScreen.Execute(*state.Config, state.Host, r.Platform, []romm.Rom{r.Game}, allGames, searchFilter, r.SelectedFileID) +} + +func executeMultiDownloadUI(state *AppState, r ui.GameListOutput) { + downloadScreen := ui.NewDownloadScreen() + downloadScreen.Execute(*state.Config, state.Host, r.Platform, r.SelectedGames, r.AllGames, r.SearchFilter, 0) +} + +func handlePlatformMappingUpdateUI(state *AppState, r ui.PlatformMappingOutput) { + state.Config.DirectoryMappings = r.Mappings + state.Config.PlatformOrder = internal.PrunePlatformOrder(state.Config.PlatformOrder, r.Mappings) + internal.SaveConfig(state.Config) + + platforms, err := internal.GetMappedPlatforms(state.Host, r.Mappings, state.Config.ApiTimeout) + if err != nil { + gaba.GetLogger().Error("Failed to refresh platforms after mapping update", "error", err) + return + } + + oldIDs := make(map[int]bool) + for _, p := range state.Platforms { + oldIDs[p.ID] = true + } + + var newPlatforms []romm.Platform + for _, p := range platforms { + if !oldIDs[p.ID] { + newPlatforms = append(newPlatforms, p) + } + } + + state.Platforms = platforms + + if len(newPlatforms) > 0 && state.CacheSync != nil { + state.CacheSync.SyncPlatforms(newPlatforms) + } +} + +func handleLogout(state *AppState) { + logger := gaba.GetLogger() + + if err := cache.DeleteCacheFolder(); err != nil { + logger.Error("Failed to delete cache folder", "error", err) + } + + state.Config.Hosts = nil + state.Config.DirectoryMappings = nil + state.Config.PlatformOrder = nil + + if err := internal.SaveConfig(state.Config); err != nil { + logger.Error("Failed to save config after logout", "error", err) + return + } + + logger.Info("User logged out successfully") + + loginConfig, err := ui.LoginFlow(romm.Host{}) + if err != nil { + logger.Error("Login flow failed after logout", "error", err) + return + } + + state.Config.Hosts = loginConfig.Hosts + if err := internal.SaveConfig(state.Config); err != nil { + logger.Error("Failed to save config after re-login", "error", err) + return + } + + state.Host = state.Config.Hosts[0] + + if len(state.Config.DirectoryMappings) == 0 { + screen := ui.NewPlatformMappingScreen() + result, err := screen.Draw(ui.PlatformMappingInput{ + Host: state.Config.Hosts[0], + ApiTimeout: state.Config.ApiTimeout, + CFW: state.CFW, + RomDirectory: cfw.GetRomDirectory(), + AutoSelect: false, + HideBackButton: true, + PlatformsBinding: state.Config.PlatformsBinding, + }) + + if err == nil && result.Action == ui.PlatformMappingActionSaved { + state.Config.DirectoryMappings = result.Mappings + internal.SaveConfig(state.Config) + } + } + + if err := cache.InitCacheManager(state.Config.Hosts[0], state.Config); err != nil { + logger.Error("Failed to initialize cache manager after re-login", "error", err) + } + + platforms, err := internal.GetMappedPlatforms(state.Config.Hosts[0], state.Config.DirectoryMappings, state.Config.ApiTimeout) + if err != nil { + logger.Error("Failed to load platforms after re-login", "error", err) + return + } + state.Platforms = platforms + + if cm := cache.GetCacheManager(); cm != nil && cm.IsFirstRun() { + progress := uatomic.NewFloat64(0) + gaba.ProcessMessage( + i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), + gaba.ProcessMessageOptions{ + ShowThemeBackground: true, + ShowProgressBar: true, + Progress: progress, + }, + func() (interface{}, error) { + _, err := cm.PopulateFullCacheWithProgress(platforms, progress) + return nil, err + }, + ) + } +} diff --git a/app/router.go b/app/router.go new file mode 100644 index 0000000..2f31a2a --- /dev/null +++ b/app/router.go @@ -0,0 +1,199 @@ +package main + +import ( + "grout/cache" + "grout/cfw" + "grout/internal" + "grout/romm" + "grout/sync" + "grout/ui" + "grout/update" + + gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/router" + goi18n "github.com/nicksnyder/go-i18n/v2/i18n" + uatomic "go.uber.org/atomic" +) + +func runWithRouter(config *internal.Config, currentCFW cfw.CFW, platforms []romm.Platform, quitOnBack bool, showCollections bool) error { + state := &AppState{ + Config: config, + Host: config.Hosts[0], + CFW: currentCFW, + Platforms: platforms, + } + currentAppState = state + + r := buildRouter(state, quitOnBack, showCollections) + + initialInput := ui.PlatformSelectionInput{ + Platforms: platforms, + QuitOnBack: quitOnBack, + ShowCollections: showCollections, + ShowSaveSync: computeShowSaveSync(state), + } + + return r.Run(ScreenPlatformSelection, initialInput) +} + +func buildRouter(state *AppState, quitOnBack bool, showCollections bool) *router.Router { + r := router.New() + + state.CacheSync = cache.NewBackgroundSync(state.Platforms) + ui.AddStatusBarIcon(state.CacheSync.Icon()) + + if cm := cache.GetCacheManager(); cm != nil && cm.IsFirstRun() { + progress := uatomic.NewFloat64(0) + gaba.ProcessMessage( + i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), + gaba.ProcessMessageOptions{ + ShowThemeBackground: true, + ShowProgressBar: true, + Progress: progress, + }, + func() (interface{}, error) { + _, err := cm.PopulateFullCacheWithProgress(state.Platforms, progress) + return nil, err + }, + ) + state.CacheSync.SetSynced() + } else { + state.CacheSync.Start() + } + + cache.RunArtworkValidation() + + registerScreens(r, state) + r.OnTransition(buildTransitionFunc(state, quitOnBack, showCollections)) + + return r +} + +func registerScreens(r *router.Router, state *AppState) { + r.Register(ScreenPlatformSelection, func(input any) (any, error) { + in := input.(ui.PlatformSelectionInput) + + if state.Config.SaveSyncMode == internal.SaveSyncModeAutomatic { + state.autoSyncOnce.Do(func() { + state.AutoSync = sync.NewAutoSync(state.Host, state.Config) + ui.AddStatusBarIcon(state.AutoSync.Icon()) + state.AutoSync.Start() + }) + } + + state.autoUpdateOnce.Do(func() { + state.AutoUpdate = update.NewAutoUpdate(state.CFW, state.Config.ReleaseChannel, &state.Host) + ui.AddStatusBarIcon(state.AutoUpdate.Icon()) + state.AutoUpdate.Start() + }) + + if in.ShowSaveSync == nil { + in.ShowSaveSync = computeShowSaveSync(state) + } + + screen := ui.NewPlatformSelectionScreen() + return screen.Draw(in) + }) + + r.Register(ScreenGameList, func(input any) (any, error) { + screen := ui.NewGameListScreen() + return screen.Draw(input.(ui.GameListInput)) + }) + + r.Register(ScreenGameDetails, func(input any) (any, error) { + screen := ui.NewGameDetailsScreen() + return screen.Draw(input.(ui.GameDetailsInput)) + }) + + r.Register(ScreenGameOptions, func(input any) (any, error) { + screen := ui.NewGameOptionsScreen() + return screen.Draw(input.(ui.GameOptionsInput)) + }) + + r.Register(ScreenSearch, func(input any) (any, error) { + screen := ui.NewSearchScreen() + return screen.Draw(input.(ui.SearchInput)) + }) + + r.Register(ScreenCollectionList, func(input any) (any, error) { + screen := ui.NewCollectionSelectionScreen() + return screen.Draw(input.(ui.CollectionSelectionInput)) + }) + + r.Register(ScreenCollectionPlatformSelection, func(input any) (any, error) { + screen := ui.NewCollectionPlatformSelectionScreen() + return screen.Draw(input.(ui.CollectionPlatformSelectionInput)) + }) + + r.Register(ScreenSettings, func(input any) (any, error) { + screen := ui.NewSettingsScreen() + return screen.Draw(input.(ui.SettingsInput)) + }) + + r.Register(ScreenGeneralSettings, func(input any) (any, error) { + screen := ui.NewGeneralSettingsScreen() + return screen.Draw(input.(ui.GeneralSettingsInput)) + }) + + r.Register(ScreenCollectionsSettings, func(input any) (any, error) { + screen := ui.NewCollectionsSettingsScreen() + return screen.Draw(input.(ui.CollectionsSettingsInput)) + }) + + r.Register(ScreenAdvancedSettings, func(input any) (any, error) { + screen := ui.NewAdvancedSettingsScreen() + return screen.Draw(input.(ui.AdvancedSettingsInput)) + }) + + r.Register(ScreenPlatformMapping, func(input any) (any, error) { + screen := ui.NewPlatformMappingScreen() + return screen.Draw(input.(ui.PlatformMappingInput)) + }) + + r.Register(ScreenSaveSyncSettings, func(input any) (any, error) { + screen := ui.NewSaveSyncSettingsScreen() + return screen.Draw(input.(ui.SaveSyncSettingsInput)) + }) + + r.Register(ScreenInfo, func(input any) (any, error) { + screen := ui.NewInfoScreen() + return screen.Draw(input.(ui.InfoInput)) + }) + + r.Register(ScreenLogoutConfirmation, func(input any) (any, error) { + screen := ui.NewLogoutConfirmationScreen() + return screen.Draw() + }) + + r.Register(ScreenRebuildCache, func(input any) (any, error) { + in := input.(ui.RebuildCacheInput) + in.CacheSync = state.CacheSync + screen := ui.NewRebuildCacheScreen() + return screen.Draw(in) + }) + + r.Register(ScreenSaveSync, func(input any) (any, error) { + screen := ui.NewSaveSyncScreen() + return screen.Draw(input.(ui.SaveSyncInput)) + }) + + r.Register(ScreenBIOSDownload, func(input any) (any, error) { + in := input.(ui.BIOSDownloadInput) + screen := ui.NewBIOSDownloadScreen() + screen.Execute(in.Config, in.Host, in.Platform) + return ui.BIOSDownloadOutput{Platform: in.Platform}, nil + }) + + r.Register(ScreenArtworkSync, func(input any) (any, error) { + in := input.(ui.ArtworkSyncInput) + screen := ui.NewArtworkSyncScreen() + screen.Execute(in.Config, in.Host) + return ui.ArtworkSyncOutput{}, nil + }) + + r.Register(ScreenUpdateCheck, func(input any) (any, error) { + screen := ui.NewUpdateScreen() + return screen.Draw(input.(ui.UpdateInput)) + }) +} diff --git a/app/screens.go b/app/screens.go new file mode 100644 index 0000000..6a95b58 --- /dev/null +++ b/app/screens.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/router" +) + +// Screen identifiers for the router +type Screen = router.Screen + +const ( + ScreenPlatformSelection Screen = iota + ScreenGameList + ScreenGameDetails + ScreenGameOptions + ScreenSearch + ScreenCollectionList + ScreenCollectionPlatformSelection + ScreenSettings + ScreenGeneralSettings + ScreenCollectionsSettings + ScreenAdvancedSettings + ScreenPlatformMapping + ScreenSaveSyncSettings + ScreenInfo + ScreenLogoutConfirmation + ScreenRebuildCache + ScreenSaveSync + ScreenBIOSDownload + ScreenArtworkSync + ScreenUpdateCheck +) diff --git a/app/setup.go b/app/setup.go index 420dd5b..31bfef1 100644 --- a/app/setup.go +++ b/app/setup.go @@ -126,7 +126,7 @@ func setup() SetupResult { } if config.LogLevel != "" { - gaba.SetRawLogLevel(config.LogLevel) + gaba.SetRawLogLevel(string(config.LogLevel)) } if config.Language != "" && !isFirstLaunch { @@ -169,8 +169,8 @@ func setup() SetupResult { PlatformsBinding: config.PlatformsBinding, }) - if err == nil && result.ExitCode == gaba.ExitCodeSuccess { - config.DirectoryMappings = result.Value.Mappings + if err == nil && result.Action == ui.PlatformMappingActionSaved { + config.DirectoryMappings = result.Mappings internal.SaveConfig(config) } } diff --git a/app/state.go b/app/state.go new file mode 100644 index 0000000..8109ac4 --- /dev/null +++ b/app/state.go @@ -0,0 +1,42 @@ +package main + +import ( + "grout/cache" + "grout/cfw" + "grout/internal" + "grout/romm" + "grout/sync" + "grout/update" + gosync "sync" + "sync/atomic" +) + +var currentAppState *AppState + +type AppState struct { + Config *internal.Config + Host romm.Host + CFW cfw.CFW + Platforms []romm.Platform + + AutoSync *sync.AutoSync + AutoUpdate *update.AutoUpdate + CacheSync *cache.BackgroundSync + + autoSyncOnce gosync.Once + autoUpdateOnce gosync.Once +} + +func computeShowSaveSync(state *AppState) *atomic.Bool { + switch state.Config.SaveSyncMode { + case internal.SaveSyncModeManual: + showSaveSync := &atomic.Bool{} + showSaveSync.Store(true) + return showSaveSync + case internal.SaveSyncModeAutomatic: + if state.AutoSync != nil { + return state.AutoSync.ShowButton() + } + } + return nil +} diff --git a/app/states.go b/app/states.go deleted file mode 100644 index 9140d6a..0000000 --- a/app/states.go +++ /dev/null @@ -1,994 +0,0 @@ -package main - -import ( - "grout/cache" - "grout/cfw" - "grout/internal" - "grout/internal/constants" - "grout/romm" - "grout/sync" - "grout/ui" - "grout/update" - "os" - gosync "sync" - "sync/atomic" - - gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" - "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" - goi18n "github.com/nicksnyder/go-i18n/v2/i18n" - uatomic "go.uber.org/atomic" -) - -var ( - autoSync *sync.AutoSync - autoSyncOnce gosync.Once - autoUpdate *update.AutoUpdate - autoUpdateOnce gosync.Once - cacheSync *cache.BackgroundSync -) - -const ( - platformSelection gaba.StateName = "platform_selection" - gameList gaba.StateName = "game_list" - gameDetails gaba.StateName = "game_details" - gameOptions gaba.StateName = "game_options" - collectionList gaba.StateName = "collection_list" - collectionPlatformSelection gaba.StateName = "collection_platform_selection" - search gaba.StateName = "search" - collectionSearch gaba.StateName = "collection_search" - settings gaba.StateName = "settings" - generalSettings gaba.StateName = "general_settings" - collectionsSettings gaba.StateName = "collections_settings" - advancedSettings gaba.StateName = "advanced_settings" - settingsPlatformMapping gaba.StateName = "platform_mapping" - saveSyncSettings gaba.StateName = "save_sync_settings" - info gaba.StateName = "info" - logoutConfirmation gaba.StateName = "logout_confirmation" - rebuildCache gaba.StateName = "rebuild_cache" - saveSync gaba.StateName = "save_sync" - biosDownload gaba.StateName = "bios_download" - artworkSync gaba.StateName = "artwork_sync" - updateCheck gaba.StateName = "update_check" -) - -type ListPosition struct { - Index int - VisibleStartIndex int -} - -type NavState struct { - CurrentGames []romm.Rom - FullGames []romm.Rom - SearchFilter string - HasBIOS bool - GameListPos ListPosition - - CollectionSearchFilter string - CollectionGames []romm.Rom - CollectionListPos ListPosition - CollectionPlatformPos ListPosition - - PlatformListPos ListPosition - - SettingsPos ListPosition - CollectionsSettingsPos ListPosition - AdvancedSettingsPos ListPosition - - QuitOnBack bool - ShowCollections bool -} - -func (s *NavState) ResetGameList() { - s.CurrentGames = nil - s.FullGames = nil - s.SearchFilter = "" - s.HasBIOS = false - s.GameListPos = ListPosition{} -} - -func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, quitOnBack bool, showCollections bool) *gaba.FSM { - fsm := gaba.NewFSM() - - nav := &NavState{ - QuitOnBack: quitOnBack, - ShowCollections: showCollections, - } - - gaba.Set(fsm.Context(), config) - gaba.Set(fsm.Context(), c) - gaba.Set(fsm.Context(), config.Hosts[0]) - gaba.Set(fsm.Context(), platforms) - gaba.Set(fsm.Context(), nav) - - // Create background sync object (for status bar icon) - cacheSync = cache.NewBackgroundSync(platforms) - ui.AddStatusBarIcon(cacheSync.Icon()) - - // If no cache exists, show progress screen and build cache - // Otherwise start background sync for incremental updates - if cm := cache.GetCacheManager(); cm != nil && cm.IsFirstRun() { - progress := uatomic.NewFloat64(0) - gaba.ProcessMessage( - i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), - gaba.ProcessMessageOptions{ - ShowThemeBackground: true, - ShowProgressBar: true, - Progress: progress, - }, - func() (interface{}, error) { - _, err := cm.PopulateFullCacheWithProgress(platforms, progress) - return nil, err - }, - ) - cacheSync.SetSynced() - } else { - cacheSync.Start() - } - - // Validate artwork cache in background - cache.RunArtworkValidation() - - gaba.AddState(fsm, platformSelection, func(ctx *gaba.Context) (ui.PlatformSelectionOutput, gaba.ExitCode) { - platforms, _ := gaba.Get[[]romm.Platform](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewPlatformSelectionScreen() - config, _ := gaba.Get[*internal.Config](ctx) - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - - // Start auto-sync on first platform menu view - if config.SaveSyncMode == "automatic" { - autoSyncOnce.Do(func() { - host, _ := gaba.Get[romm.Host](ctx) - autoSync = sync.NewAutoSync(host, config) - ui.AddStatusBarIcon(autoSync.Icon()) - autoSync.Start() - }) - } - - autoUpdateOnce.Do(func() { - host, _ := gaba.Get[romm.Host](ctx) - autoUpdate = update.NewAutoUpdate(currentCFW, config.ReleaseChannel, &host) - ui.AddStatusBarIcon(autoUpdate.Icon()) - autoUpdate.Start() - }) - - // Determine the sync button visibility control - // - "off": nil (never show) - // - "manual": always true, shows "Sync" button - // - "automatic": controlled by auto-sync - var showSaveSync *atomic.Bool - switch config.SaveSyncMode { - case "manual": - showSaveSync = &atomic.Bool{} - showSaveSync.Store(true) - case "automatic": - if autoSync != nil { - showSaveSync = autoSync.ShowButton() - } - } - - result, err := screen.Draw(ui.PlatformSelectionInput{ - Platforms: platforms, - QuitOnBack: nav.QuitOnBack, - ShowCollections: nav.ShowCollections, - ShowSaveSync: showSaveSync, - LastSelectedIndex: nav.PlatformListPos.Index, - LastSelectedPosition: nav.PlatformListPos.VisibleStartIndex, - }) - - if err != nil { - return ui.PlatformSelectionOutput{}, gaba.ExitCodeError - } - - nav.PlatformListPos.Index = result.Value.LastSelectedIndex - nav.PlatformListPos.VisibleStartIndex = result.Value.LastSelectedPosition - - if len(result.Value.ReorderedPlatforms) > 0 { - config, _ := gaba.Get[*internal.Config](ctx) - - var platformOrder []string - for _, p := range result.Value.ReorderedPlatforms { - platformOrder = append(platformOrder, p.Slug) - } - - gaba.GetLogger().Debug("Saving platform order to config", "order", platformOrder) - - config.PlatformOrder = platformOrder - if err := internal.SaveConfig(config); err != nil { - gaba.GetLogger().Error("Failed to save platform order", "error", err) - } else { - gaba.GetLogger().Info("Platform order saved successfully", "order", platformOrder) - } - - gaba.Set(ctx, result.Value.ReorderedPlatforms) - gaba.GetLogger().Debug("Updated platforms in context") - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, gameList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.ResetGameList() - gaba.Set(ctx, ui.CollectionSelectionOutput{}) - return nil - }). - OnWithHook(constants.ExitCodeCollections, collectionList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.CollectionListPos = ListPosition{} - return nil - }). - On(gaba.ExitCodeAction, settings). - On(constants.ExitCodeSaveSync, saveSync). - Exit(gaba.ExitCodeQuit) - - gaba.AddState(fsm, collectionList, func(ctx *gaba.Context) (ui.CollectionSelectionOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewCollectionSelectionScreen() - result, err := screen.Draw(ui.CollectionSelectionInput{ - Config: config, - Host: host, - SearchFilter: nav.CollectionSearchFilter, - LastSelectedIndex: nav.CollectionListPos.Index, - LastSelectedPosition: nav.CollectionListPos.VisibleStartIndex, - }) - - if err != nil { - return ui.CollectionSelectionOutput{}, gaba.ExitCodeError - } - - nav.CollectionListPos.Index = result.Value.LastSelectedIndex - nav.CollectionListPos.VisibleStartIndex = result.Value.LastSelectedPosition - nav.CollectionSearchFilter = result.Value.SearchFilter - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, collectionPlatformSelection, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.ResetGameList() - nav.CollectionPlatformPos = ListPosition{} - nav.CollectionGames = nil - gaba.Set(ctx, ui.PlatformSelectionOutput{}) - return nil - }). - On(constants.ExitCodeSearch, collectionSearch). - OnWithHook(constants.ExitCodeClearSearch, collectionList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.CollectionSearchFilter = "" - nav.CollectionListPos = ListPosition{} - return nil - }). - On(gaba.ExitCodeBack, platformSelection) - - gaba.AddState(fsm, collectionPlatformSelection, func(ctx *gaba.Context) (ui.CollectionPlatformSelectionOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - collection, _ := gaba.Get[ui.CollectionSelectionOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewCollectionPlatformSelectionScreen() - result, err := screen.Draw(ui.CollectionPlatformSelectionInput{ - Config: config, - Host: host, - Collection: collection.SelectedCollection, - CachedGames: nav.CollectionGames, - LastSelectedIndex: nav.CollectionPlatformPos.Index, - LastSelectedPosition: nav.CollectionPlatformPos.VisibleStartIndex, - }) - - if err != nil { - return ui.CollectionPlatformSelectionOutput{}, gaba.ExitCodeError - } - - nav.CollectionPlatformPos.Index = result.Value.LastSelectedIndex - nav.CollectionPlatformPos.VisibleStartIndex = result.Value.LastSelectedPosition - nav.CollectionGames = result.Value.AllGames - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, gameList, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.CollectionPlatformSelectionOutput](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - var finalGames []romm.Rom - - // In unified mode with Platform.ID == 0, use all games - if config.CollectionView == "unified" && output.SelectedPlatform.ID == 0 { - finalGames = output.AllGames - } else { - // Platform mode: filter by selected platform - filteredGames := make([]romm.Rom, 0) - for _, game := range output.AllGames { - if game.PlatformID == output.SelectedPlatform.ID { - filteredGames = append(filteredGames, game) - } - } - finalGames = filteredGames - } - - nav.SearchFilter = "" - nav.FullGames = finalGames - nav.CurrentGames = finalGames - nav.GameListPos = ListPosition{} - return nil - }). - On(gaba.ExitCodeBack, collectionList) - - gaba.AddState(fsm, gameList, func(ctx *gaba.Context) (ui.GameListOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - platform, _ := gaba.Get[ui.PlatformSelectionOutput](ctx) - collection, _ := gaba.Get[ui.CollectionSelectionOutput](ctx) - collectionPlatform, _ := gaba.Get[ui.CollectionPlatformSelectionOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - var selectedPlatform romm.Platform - var selectedCollection romm.Collection - - if collectionPlatform.SelectedPlatform.ID != 0 { - selectedPlatform = collectionPlatform.SelectedPlatform - selectedCollection = collectionPlatform.Collection - } else { - selectedPlatform = platform.SelectedPlatform - selectedCollection = collection.SelectedCollection - } - - screen := ui.NewGameListScreen() - result, err := screen.Draw(ui.GameListInput{ - Config: config, - Host: host, - Platform: selectedPlatform, - Collection: selectedCollection, - Games: nav.CurrentGames, - HasBIOS: nav.HasBIOS, - SearchFilter: nav.SearchFilter, - LastSelectedIndex: nav.GameListPos.Index, - LastSelectedPosition: nav.GameListPos.VisibleStartIndex, - }) - - if err != nil { - return ui.GameListOutput{}, gaba.ExitCodeError - } - - nav.FullGames = result.Value.AllGames - nav.CurrentGames = result.Value.AllGames - nav.HasBIOS = result.Value.HasBIOS - nav.GameListPos.Index = result.Value.LastSelectedIndex - nav.GameListPos.VisibleStartIndex = result.Value.LastSelectedPosition - nav.SearchFilter = result.Value.SearchFilter - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeSuccess, gameDetails). - On(constants.ExitCodeSearch, search). - On(constants.ExitCodeBIOS, biosDownload). - OnWithHook(constants.ExitCodeClearSearch, gameList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.SearchFilter = "" - nav.CurrentGames = nav.FullGames - nav.GameListPos = ListPosition{} - return nil - }). - OnWithHook(gaba.ExitCodeBack, platformSelection, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.CurrentGames = nil - return nil - }). - On(constants.ExitCodeBackToCollectionPlatform, collectionPlatformSelection). - OnWithHook(constants.ExitCodeBackToCollection, collectionList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.CurrentGames = nil - return nil - }). - On(constants.ExitCodeNoResults, search) - - gaba.AddState(fsm, gameDetails, func(ctx *gaba.Context) (ui.GameDetailsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - gameListOutput, _ := gaba.Get[ui.GameListOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - // If multiple games selected, skip details and go straight to download - if len(gameListOutput.SelectedGames) != 1 { - downloadScreen := ui.NewDownloadScreen() - downloadOutput := downloadScreen.Execute(*config, host, gameListOutput.Platform, gameListOutput.SelectedGames, gameListOutput.AllGames, nav.SearchFilter, 0) - nav.CurrentGames = downloadOutput.AllGames - nav.SearchFilter = downloadOutput.SearchFilter - triggerAutoSync() - return ui.GameDetailsOutput{}, gaba.ExitCodeBack - } - - screen := ui.NewGameDetailsScreen() - result, err := screen.Draw(ui.GameDetailsInput{ - Config: config, - Host: host, - Platform: gameListOutput.Platform, - Game: gameListOutput.SelectedGames[0], - }) - - if err != nil { - return ui.GameDetailsOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(constants.ExitCodeDownloadRequested, gameDetails, func(ctx *gaba.Context) error { - detailsOutput, _ := gaba.Get[ui.GameDetailsOutput](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - gameListOutput, _ := gaba.Get[ui.GameListOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - downloadScreen := ui.NewDownloadScreen() - downloadOutput := downloadScreen.Execute(*config, host, detailsOutput.Platform, []romm.Rom{detailsOutput.Game}, gameListOutput.AllGames, nav.SearchFilter, detailsOutput.SelectedFileID) - nav.CurrentGames = downloadOutput.AllGames - nav.SearchFilter = downloadOutput.SearchFilter - triggerAutoSync() - - return nil - }). - On(gaba.ExitCodeSuccess, gameList). - On(gaba.ExitCodeBack, gameList). - On(constants.ExitCodeGameOptions, gameOptions) - - // Game options state - gaba.AddState(fsm, gameOptions, func(ctx *gaba.Context) (ui.GameOptionsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - gameListOutput, _ := gaba.Get[ui.GameListOutput](ctx) - - if len(gameListOutput.SelectedGames) != 1 { - return ui.GameOptionsOutput{Config: config}, gaba.ExitCodeBack - } - - screen := ui.NewGameOptionsScreen() - result, err := screen.Draw(ui.GameOptionsInput{ - Config: config, - Game: gameListOutput.SelectedGames[0], - }) - - if err != nil { - return ui.GameOptionsOutput{Config: config}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, gameDetails, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.GameOptionsOutput](ctx) - gaba.Set(ctx, output.Config) - return nil - }). - On(gaba.ExitCodeBack, gameDetails) - - gaba.AddState(fsm, search, func(ctx *gaba.Context) (ui.SearchOutput, gaba.ExitCode) { - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewSearchScreen() - result, err := screen.Draw(ui.SearchInput{ - InitialText: nav.SearchFilter, - }) - - if err != nil { - return ui.SearchOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, gameList, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.SearchOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - nav.SearchFilter = output.Query - nav.CurrentGames = nav.FullGames - nav.GameListPos = ListPosition{} - return nil - }). - OnWithHook(gaba.ExitCodeBack, gameList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.SearchFilter = "" - nav.CurrentGames = nav.FullGames - return nil - }) - - gaba.AddState(fsm, collectionSearch, func(ctx *gaba.Context) (ui.SearchOutput, gaba.ExitCode) { - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewSearchScreen() - result, err := screen.Draw(ui.SearchInput{ - InitialText: nav.CollectionSearchFilter, - }) - - if err != nil { - return ui.SearchOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, collectionList, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.SearchOutput](ctx) - nav, _ := gaba.Get[*NavState](ctx) - nav.CollectionSearchFilter = output.Query - nav.CollectionListPos = ListPosition{} - return nil - }). - OnWithHook(gaba.ExitCodeBack, collectionList, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.CollectionSearchFilter = "" - return nil - }) - - gaba.AddState(fsm, settings, func(ctx *gaba.Context) (ui.SettingsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - host, _ := gaba.Get[romm.Host](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewSettingsScreen() - result, err := screen.Draw(ui.SettingsInput{ - Config: config, - CFW: currentCFW, - Host: host, - LastSelectedIndex: nav.SettingsPos.Index, - LastVisibleStartIndex: nav.SettingsPos.VisibleStartIndex, - }) - - if err != nil { - return ui.SettingsOutput{}, gaba.ExitCodeError - } - - nav.SettingsPos.Index = result.Value.LastSelectedIndex - nav.SettingsPos.VisibleStartIndex = result.Value.LastVisibleStartIndex - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, platformSelection, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.SettingsOutput](ctx) - host, _ := gaba.Get[romm.Host](ctx) - nav, _ := gaba.Get[*NavState](ctx) - internal.SaveConfig(output.Config) - gaba.Set(ctx, output.Config) - nav.SettingsPos = ListPosition{} - - nav.ShowCollections = output.Config.ShowCollections(host) - return nil - }). - On(constants.ExitCodeGeneralSettings, generalSettings). - On(constants.ExitCodeCollectionsSettings, collectionsSettings). - On(constants.ExitCodeEditMappings, settingsPlatformMapping). - On(constants.ExitCodeAdvancedSettings, advancedSettings). - On(constants.ExitCodeSaveSyncSettings, saveSyncSettings). - On(constants.ExitCodeInfo, info). - On(constants.ExitCodeCheckUpdate, updateCheck). - OnWithHook(gaba.ExitCodeBack, platformSelection, func(ctx *gaba.Context) error { - nav, _ := gaba.Get[*NavState](ctx) - nav.SettingsPos = ListPosition{} - return nil - }) - - gaba.AddState(fsm, generalSettings, func(ctx *gaba.Context) (ui.GeneralSettingsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - - screen := ui.NewGeneralSettingsScreen() - result, err := screen.Draw(ui.GeneralSettingsInput{ - Config: config, - }) - - if err != nil { - return ui.GeneralSettingsOutput{Config: config}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, settings, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.GeneralSettingsOutput](ctx) - gaba.Set(ctx, output.Config) - return nil - }). - On(gaba.ExitCodeBack, settings) - - gaba.AddState(fsm, collectionsSettings, func(ctx *gaba.Context) (ui.CollectionsSettingsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - - screen := ui.NewCollectionsSettingsScreen() - result, err := screen.Draw(ui.CollectionsSettingsInput{ - Config: config, - }) - - if err != nil { - return ui.CollectionsSettingsOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, settings, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.CollectionsSettingsOutput](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - nav, _ := gaba.Get[*NavState](ctx) - nav.CollectionsSettingsPos = ListPosition{} - nav.ShowCollections = config.ShowCollections(host) - - if output.SyncNeeded && cacheSync != nil { - cacheSync.SyncCollections() - } - return nil - }). - On(gaba.ExitCodeBack, settings) - - gaba.AddState(fsm, saveSyncSettings, func(ctx *gaba.Context) (ui.SaveSyncSettingsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - - screen := ui.NewSaveSyncSettingsScreen() - result, err := screen.Draw(ui.SaveSyncSettingsInput{ - Config: config, - CFW: currentCFW, - }) - - if err != nil { - return ui.SaveSyncSettingsOutput{Config: config}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, settings, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.SaveSyncSettingsOutput](ctx) - gaba.Set(ctx, output.Config) - triggerAutoSync() - return nil - }). - On(gaba.ExitCodeBack, settings) - - gaba.AddState(fsm, advancedSettings, func(ctx *gaba.Context) (ui.AdvancedSettingsOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - nav, _ := gaba.Get[*NavState](ctx) - - screen := ui.NewAdvancedSettingsScreen() - result, err := screen.Draw(ui.AdvancedSettingsInput{ - Config: config, - Host: host, - LastSelectedIndex: nav.AdvancedSettingsPos.Index, - LastVisibleStartIndex: nav.AdvancedSettingsPos.VisibleStartIndex, - }) - - if err != nil { - return ui.AdvancedSettingsOutput{}, gaba.ExitCodeError - } - - nav.AdvancedSettingsPos.Index = result.Value.LastSelectedIndex - nav.AdvancedSettingsPos.VisibleStartIndex = result.Value.LastVisibleStartIndex - - if result.ExitCode == gaba.ExitCodeSuccess && autoUpdate != nil { - autoUpdate.Recheck(config.ReleaseChannel) - } - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeSuccess, settings). - On(constants.ExitCodeRebuildCache, rebuildCache). - On(constants.ExitCodeSyncArtwork, artworkSync). - On(gaba.ExitCodeBack, settings) - - gaba.AddState(fsm, settingsPlatformMapping, func(ctx *gaba.Context) (ui.PlatformMappingOutput, gaba.ExitCode) { - host, _ := gaba.Get[romm.Host](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - - screen := ui.NewPlatformMappingScreen() - result, err := screen.Draw(ui.PlatformMappingInput{ - Host: host, - ApiTimeout: config.ApiTimeout, - CFW: currentCFW, - RomDirectory: cfw.GetRomDirectory(), - AutoSelect: false, - HideBackButton: false, - ExistingMappings: config.DirectoryMappings, // Pass existing mappings for return visits - PlatformsBinding: config.PlatformsBinding, - }) - - if err != nil { - return ui.PlatformMappingOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - OnWithHook(gaba.ExitCodeSuccess, settings, func(ctx *gaba.Context) error { - output, _ := gaba.Get[ui.PlatformMappingOutput](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - oldPlatforms, _ := gaba.Get[[]romm.Platform](ctx) - - // Track old platform IDs - oldPlatformIDs := make(map[int]bool) - for _, p := range oldPlatforms { - oldPlatformIDs[p.ID] = true - } - - config.DirectoryMappings = output.Mappings - config.PlatformOrder = internal.PrunePlatformOrder(config.PlatformOrder, output.Mappings) - internal.SaveConfig(config) - gaba.Set(ctx, config) - - platforms, err := internal.GetMappedPlatforms(host, output.Mappings, config.ApiTimeout) - if err != nil { - gaba.GetLogger().Error("Failed to load platforms", "error", err) - return err - } - gaba.Set(ctx, platforms) - - // Find newly added platforms - var newPlatforms []romm.Platform - for _, p := range platforms { - if !oldPlatformIDs[p.ID] { - newPlatforms = append(newPlatforms, p) - } - } - - // Sync games for new platforms - if len(newPlatforms) > 0 && cacheSync != nil { - gaba.GetLogger().Debug("Syncing games for new platforms", "count", len(newPlatforms)) - cacheSync.SyncPlatforms(newPlatforms) - } - - return nil - }). - On(gaba.ExitCodeBack, settings) - - gaba.AddState(fsm, info, func(ctx *gaba.Context) (ui.InfoOutput, gaba.ExitCode) { - host, _ := gaba.Get[romm.Host](ctx) - - screen := ui.NewInfoScreen() - result, err := screen.Draw(ui.InfoInput{ - Host: host, - }) - - if err != nil { - return ui.InfoOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeBack, settings). - On(constants.ExitCodeLogoutConfirm, logoutConfirmation) - - gaba.AddState(fsm, logoutConfirmation, func(ctx *gaba.Context) (ui.LogoutConfirmationOutput, gaba.ExitCode) { - screen := ui.NewLogoutConfirmationScreen() - result, err := screen.Draw() - - if err != nil { - return ui.LogoutConfirmationOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeBack, info). - OnWithHook(constants.ExitCodeLogout, platformSelection, func(ctx *gaba.Context) error { - config, _ := gaba.Get[*internal.Config](ctx) - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - - // Delete the entire cache folder on logout - if err := cache.DeleteCacheFolder(); err != nil { - gaba.GetLogger().Error("Failed to delete cache folder", "error", err) - // Continue with logout even if cache deletion fails - } - - config.Hosts = nil - config.DirectoryMappings = nil - config.PlatformOrder = nil - - if err := internal.SaveConfig(config); err != nil { - gaba.GetLogger().Error("Failed to save config after logout", "error", err) - return err - } - - gaba.GetLogger().Info("User logged out successfully") - - loginConfig, err := ui.LoginFlow(romm.Host{}) - if err != nil { - gaba.GetLogger().Error("Login flow failed after logout", "error", err) - return err - } - - config.Hosts = loginConfig.Hosts - if err := internal.SaveConfig(config); err != nil { - gaba.GetLogger().Error("Failed to save config after re-login", "error", err) - return err - } - - gaba.Set(ctx, config) - gaba.Set(ctx, config.Hosts[0]) - - if len(config.DirectoryMappings) == 0 { - screen := ui.NewPlatformMappingScreen() - result, err := screen.Draw(ui.PlatformMappingInput{ - Host: config.Hosts[0], - ApiTimeout: config.ApiTimeout, - CFW: currentCFW, - RomDirectory: cfw.GetRomDirectory(), - AutoSelect: false, - HideBackButton: true, - PlatformsBinding: config.PlatformsBinding, - }) - - if err == nil && result.ExitCode == gaba.ExitCodeSuccess { - config.DirectoryMappings = result.Value.Mappings - internal.SaveConfig(config) - } - } - - // Re-initialize cache manager for the new host - if err := cache.InitCacheManager(config.Hosts[0], config); err != nil { - gaba.GetLogger().Error("Failed to initialize cache manager after re-login", "error", err) - } - - platforms, err := internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout) - if err != nil { - gaba.GetLogger().Error("Failed to load platforms after re-login", "error", err) - return err - } - gaba.Set(ctx, platforms) - - // Populate cache for the new login - if cm := cache.GetCacheManager(); cm != nil && cm.IsFirstRun() { - progress := uatomic.NewFloat64(0) - gaba.ProcessMessage( - i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), - gaba.ProcessMessageOptions{ - ShowThemeBackground: true, - ShowProgressBar: true, - Progress: progress, - }, - func() (interface{}, error) { - _, err := cm.PopulateFullCacheWithProgress(platforms, progress) - return nil, err - }, - ) - } - - nav, _ := gaba.Get[*NavState](ctx) - nav.ResetGameList() - nav.PlatformListPos = ListPosition{} - - return nil - }) - - gaba.AddState(fsm, rebuildCache, func(ctx *gaba.Context) (struct{}, gaba.ExitCode) { - logger := gaba.GetLogger() - host, _ := gaba.Get[romm.Host](ctx) - config, _ := gaba.Get[*internal.Config](ctx) - - if cacheSync != nil { - cacheSync.Stop() - } - - if err := cache.DeleteCacheFolder(); err != nil { - logger.Error("Failed to delete cache folder", "error", err) - } - - // Re-initialize cache manager - if err := cache.InitCacheManager(host, config); err != nil { - logger.Error("Failed to reinitialize cache manager", "error", err) - return struct{}{}, gaba.ExitCodeError - } - - // Fetch fresh platform list from API - platforms, err := internal.GetMappedPlatforms(host, config.DirectoryMappings, config.ApiTimeout) - if err != nil { - logger.Error("Failed to fetch platforms", "error", err) - return struct{}{}, gaba.ExitCodeError - } - - // Apply custom platform order and update in context - platforms = internal.SortPlatformsByOrder(platforms, config.PlatformOrder) - gaba.Set(ctx, platforms) - - // Re-populate cache with progress - cm := cache.GetCacheManager() - progress := uatomic.NewFloat64(0) - gaba.ProcessMessage( - i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), - gaba.ProcessMessageOptions{ - ShowThemeBackground: true, - ShowProgressBar: true, - Progress: progress, - }, - func() (any, error) { - _, err := cm.PopulateFullCacheWithProgress(platforms, progress) - return nil, err - }, - ) - - if cacheSync != nil { - cacheSync.SetSynced() - } - - logger.Info("Cache rebuild completed") - return struct{}{}, gaba.ExitCodeSuccess - }). - On(gaba.ExitCodeSuccess, advancedSettings). - On(gaba.ExitCodeError, advancedSettings) - - gaba.AddState(fsm, saveSync, func(ctx *gaba.Context) (ui.SaveSyncOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - - screen := ui.NewSaveSyncScreen() - result, err := screen.Draw(ui.SaveSyncInput{ - Config: config, - Host: host, - }) - - if err != nil { - return ui.SaveSyncOutput{}, gaba.ExitCodeError - } - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeBack, platformSelection) - - gaba.AddState(fsm, biosDownload, func(ctx *gaba.Context) (ui.BIOSDownloadOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - platform, _ := gaba.Get[ui.PlatformSelectionOutput](ctx) - collectionPlatform, _ := gaba.Get[ui.CollectionPlatformSelectionOutput](ctx) - - var selectedPlatform romm.Platform - if collectionPlatform.SelectedPlatform.ID != 0 { - selectedPlatform = collectionPlatform.SelectedPlatform - } else { - selectedPlatform = platform.SelectedPlatform - } - - screen := ui.NewBIOSDownloadScreen() - output := screen.Execute(*config, host, selectedPlatform) - - return output, gaba.ExitCodeBack - }). - On(gaba.ExitCodeBack, gameList) - - gaba.AddState(fsm, artworkSync, func(ctx *gaba.Context) (ui.ArtworkSyncOutput, gaba.ExitCode) { - config, _ := gaba.Get[*internal.Config](ctx) - host, _ := gaba.Get[romm.Host](ctx) - - screen := ui.NewArtworkSyncScreen() - output := screen.Execute(*config, host) - - return output, gaba.ExitCodeBack - }). - On(gaba.ExitCodeBack, advancedSettings) - - gaba.AddState(fsm, updateCheck, func(ctx *gaba.Context) (ui.UpdateOutput, gaba.ExitCode) { - currentCFW, _ := gaba.Get[cfw.CFW](ctx) - host, _ := gaba.Get[romm.Host](ctx) - - screen := ui.NewUpdateScreen() - result, err := screen.Draw(ui.UpdateInput{ - CFW: currentCFW, - ReleaseChannel: config.ReleaseChannel, - Host: &host, - }) - - if err != nil { - return ui.UpdateOutput{}, gaba.ExitCodeError - } - - if result.Value.UpdatePerformed { - os.Exit(0) - } - - return result.Value, result.ExitCode - }). - On(gaba.ExitCodeBack, settings). - On(gaba.ExitCodeSuccess, settings) - - return fsm.Start(platformSelection) -} - -func triggerAutoSync() { - if autoSync != nil { - autoSync.Trigger() - } -} diff --git a/app/transitions.go b/app/transitions.go new file mode 100644 index 0000000..286a4e9 --- /dev/null +++ b/app/transitions.go @@ -0,0 +1,585 @@ +package main + +import ( + "grout/cfw" + "grout/internal" + "grout/ui" + "os" + + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/router" +) + +type transitionContext struct { + state *AppState + stack *router.Stack + quitOnBack bool + showCollections bool +} + +func buildTransitionFunc(state *AppState, quitOnBack bool, showCollections bool) router.TransitionFunc { + return func(from router.Screen, result any, stack *router.Stack) (router.Screen, any) { + ctx := &transitionContext{ + state: state, + stack: stack, + quitOnBack: quitOnBack, + showCollections: showCollections, + } + + switch from { + case ScreenPlatformSelection: + return transitionPlatformSelection(ctx, result) + case ScreenGameList: + return transitionGameList(ctx, result) + case ScreenSearch: + return transitionSearch(ctx, result) + case ScreenGameDetails: + return transitionGameDetails(ctx, result) + case ScreenGameOptions: + return transitionGameOptions(ctx, result) + case ScreenCollectionList: + return transitionCollectionList(ctx, result) + case ScreenCollectionPlatformSelection: + return transitionCollectionPlatformSelection(ctx, result) + case ScreenSettings: + return transitionSettings(ctx, result) + case ScreenGeneralSettings: + return transitionGeneralSettings(ctx, result) + case ScreenCollectionsSettings: + return transitionCollectionsSettings(ctx, result) + case ScreenAdvancedSettings: + return transitionAdvancedSettings(ctx, result) + case ScreenPlatformMapping: + return transitionPlatformMapping(ctx, result) + case ScreenSaveSyncSettings: + return transitionSaveSyncSettings(ctx, result) + case ScreenInfo: + return transitionInfo(ctx, result) + case ScreenLogoutConfirmation: + return transitionLogoutConfirmation(ctx, result) + case ScreenRebuildCache: + return transitionRebuildCache(ctx, result) + case ScreenSaveSync: + return popOrExit(stack) + case ScreenBIOSDownload: + return popOrExit(stack) + case ScreenArtworkSync: + return popOrExit(stack) + case ScreenUpdateCheck: + return transitionUpdateCheck(ctx, result) + } + + return router.ScreenExit, nil + } +} + +func transitionPlatformSelection(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.PlatformSelectionOutput) + + if len(r.ReorderedPlatforms) > 0 { + savePlatformOrder(ctx.state, r.ReorderedPlatforms) + } + + pushInput := ui.PlatformSelectionInput{ + Platforms: ctx.state.Platforms, + QuitOnBack: ctx.quitOnBack, + ShowCollections: ctx.showCollections, + ShowSaveSync: computeShowSaveSync(ctx.state), + } + + switch r.Action { + case ui.PlatformSelectionActionSelected: + ctx.stack.Push(ScreenPlatformSelection, pushInput, r) + return ScreenGameList, ui.GameListInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.SelectedPlatform, + } + + case ui.PlatformSelectionActionCollections: + ctx.stack.Push(ScreenPlatformSelection, pushInput, r) + return ScreenCollectionList, ui.CollectionSelectionInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + } + + case ui.PlatformSelectionActionSettings: + ctx.stack.Push(ScreenPlatformSelection, pushInput, r) + return ScreenSettings, ui.SettingsInput{ + Config: ctx.state.Config, + CFW: ctx.state.CFW, + Host: ctx.state.Host, + } + + case ui.PlatformSelectionActionSaveSync: + ctx.stack.Push(ScreenPlatformSelection, pushInput, r) + return ScreenSaveSync, ui.SaveSyncInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + } + + case ui.PlatformSelectionActionQuit: + return router.ScreenExit, nil + } + + return router.ScreenExit, nil +} + +func transitionGameList(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.GameListOutput) + + pushInput := ui.GameListInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + Collection: r.Collection, + Games: r.AllGames, + HasBIOS: r.HasBIOS, + SearchFilter: r.SearchFilter, + } + + switch r.Action { + case ui.GameListActionSelected: + if len(r.SelectedGames) > 1 { + executeMultiDownloadUI(ctx.state, r) + triggerAutoSyncRouter(ctx.state) + return ScreenGameList, ui.GameListInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + Collection: r.Collection, + Games: r.AllGames, + HasBIOS: r.HasBIOS, + SearchFilter: r.SearchFilter, + LastSelectedIndex: r.LastSelectedIndex, + LastSelectedPosition: r.LastSelectedPosition, + } + } + + ctx.stack.Push(ScreenGameList, pushInput, r) + return ScreenGameDetails, ui.GameDetailsInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + Game: r.SelectedGames[0], + } + + case ui.GameListActionSearch: + ctx.stack.Push(ScreenGameList, pushInput, r) + return ScreenSearch, ui.SearchInput{ + InitialText: r.SearchFilter, + } + + case ui.GameListActionClearSearch: + return ScreenGameList, ui.GameListInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + Collection: r.Collection, + Games: r.AllGames, + HasBIOS: r.HasBIOS, + } + + case ui.GameListActionBIOS: + ctx.stack.Push(ScreenGameList, pushInput, r) + return ScreenBIOSDownload, ui.BIOSDownloadInput{ + Config: *ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + } + + case ui.GameListActionBack: + return popOrExit(ctx.stack) + } + + return router.ScreenExit, nil +} + +func transitionSearch(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.SearchOutput) + + entry := ctx.stack.Pop() + if entry == nil { + return router.ScreenExit, nil + } + + switch r.Action { + case ui.SearchActionApply: + if entry.Screen == ScreenGameList { + prevInput := entry.Input.(ui.GameListInput) + return ScreenGameList, ui.GameListInput{ + Config: prevInput.Config, + Host: prevInput.Host, + Platform: prevInput.Platform, + Collection: prevInput.Collection, + Games: prevInput.Games, + HasBIOS: prevInput.HasBIOS, + SearchFilter: r.Query, + } + } + if entry.Screen == ScreenCollectionList { + prevInput := entry.Input.(ui.CollectionSelectionInput) + return ScreenCollectionList, ui.CollectionSelectionInput{ + Config: prevInput.Config, + Host: prevInput.Host, + SearchFilter: r.Query, + } + } + + case ui.SearchActionCancel: + if entry.Screen == ScreenGameList { + prevInput := entry.Input.(ui.GameListInput) + prevResume := entry.Resume.(ui.GameListOutput) + return ScreenGameList, ui.GameListInput{ + Config: prevInput.Config, + Host: prevInput.Host, + Platform: prevInput.Platform, + Collection: prevInput.Collection, + Games: prevInput.Games, + HasBIOS: prevInput.HasBIOS, + SearchFilter: prevInput.SearchFilter, + LastSelectedIndex: prevResume.LastSelectedIndex, + LastSelectedPosition: prevResume.LastSelectedPosition, + } + } + if entry.Screen == ScreenCollectionList { + prevInput := entry.Input.(ui.CollectionSelectionInput) + prevResume := entry.Resume.(ui.CollectionSelectionOutput) + return ScreenCollectionList, ui.CollectionSelectionInput{ + Config: prevInput.Config, + Host: prevInput.Host, + SearchFilter: prevInput.SearchFilter, + LastSelectedIndex: prevResume.LastSelectedIndex, + LastSelectedPosition: prevResume.LastSelectedPosition, + } + } + } + + return router.ScreenExit, nil +} + +func transitionGameDetails(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.GameDetailsOutput) + + switch r.Action { + case ui.GameDetailsActionDownload: + executeDownloadUI(ctx.state, r, ctx.stack) + triggerAutoSyncRouter(ctx.state) + return popOrExit(ctx.stack) + + case ui.GameDetailsActionOptions: + ctx.stack.Push(ScreenGameDetails, ui.GameDetailsInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.Platform, + Game: r.Game, + }, nil) + return ScreenGameOptions, ui.GameOptionsInput{ + Config: ctx.state.Config, + Game: r.Game, + } + + case ui.GameDetailsActionBack: + return popOrExit(ctx.stack) + } + + return router.ScreenExit, nil +} + +func transitionGameOptions(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.GameOptionsOutput) + if r.Config != nil { + ctx.state.Config = r.Config + } + return popOrExit(ctx.stack) +} + +func transitionCollectionList(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.CollectionSelectionOutput) + + pushInput := ui.CollectionSelectionInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + SearchFilter: r.SearchFilter, + } + + switch r.Action { + case ui.CollectionListActionSelected: + ctx.stack.Push(ScreenCollectionList, pushInput, r) + return ScreenCollectionPlatformSelection, ui.CollectionPlatformSelectionInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Collection: r.SelectedCollection, + } + + case ui.CollectionListActionSearch: + ctx.stack.Push(ScreenCollectionList, pushInput, r) + return ScreenSearch, ui.SearchInput{ + InitialText: r.SearchFilter, + } + + case ui.CollectionListActionClearSearch: + return ScreenCollectionList, ui.CollectionSelectionInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + } + + case ui.CollectionListActionBack: + return popOrExit(ctx.stack) + } + + return router.ScreenExit, nil +} + +func transitionCollectionPlatformSelection(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.CollectionPlatformSelectionOutput) + + switch r.Action { + case ui.CollectionPlatformSelectionActionSelected: + games := r.AllGames + + // In unified mode (ID=0), the platform selection screen was skipped, + // so don't push it to the stack - back should go to collection list + if r.SelectedPlatform.ID != 0 { + ctx.stack.Push(ScreenCollectionPlatformSelection, ui.CollectionPlatformSelectionInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Collection: r.Collection, + CachedGames: r.AllGames, + }, r) + games = filterGamesByPlatform(r.AllGames, r.SelectedPlatform.ID) + } + + return ScreenGameList, ui.GameListInput{ + Config: ctx.state.Config, + Host: ctx.state.Host, + Platform: r.SelectedPlatform, + Collection: r.Collection, + Games: games, + } + + case ui.CollectionPlatformSelectionActionBack: + return popOrExit(ctx.stack) + } + + return router.ScreenExit, nil +} + +func transitionSettings(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.SettingsOutput) + + if r.Config != nil { + ctx.state.Config = r.Config + internal.SaveConfig(ctx.state.Config) + ctx.showCollections = ctx.state.Config.ShowCollections(ctx.state.Host) + } + + pushInput := ui.SettingsInput{Config: ctx.state.Config, CFW: ctx.state.CFW, Host: ctx.state.Host} + + switch r.Action { + case ui.SettingsActionGeneral: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenGeneralSettings, ui.GeneralSettingsInput{Config: ctx.state.Config} + + case ui.SettingsActionCollections: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenCollectionsSettings, ui.CollectionsSettingsInput{Config: ctx.state.Config} + + case ui.SettingsActionAdvanced: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenAdvancedSettings, ui.AdvancedSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host} + + case ui.SettingsActionPlatformMapping: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenPlatformMapping, ui.PlatformMappingInput{ + Host: ctx.state.Host, + ApiTimeout: ctx.state.Config.ApiTimeout, + CFW: ctx.state.CFW, + RomDirectory: cfw.GetRomDirectory(), + ExistingMappings: ctx.state.Config.DirectoryMappings, + PlatformsBinding: ctx.state.Config.PlatformsBinding, + } + + case ui.SettingsActionSaveSync: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenSaveSyncSettings, ui.SaveSyncSettingsInput{Config: ctx.state.Config, CFW: ctx.state.CFW} + + case ui.SettingsActionInfo: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenInfo, ui.InfoInput{Host: ctx.state.Host} + + case ui.SettingsActionCheckUpdate: + ctx.stack.Push(ScreenSettings, pushInput, r) + return ScreenUpdateCheck, ui.UpdateInput{ + CFW: ctx.state.CFW, + ReleaseChannel: ctx.state.Config.ReleaseChannel, + Host: &ctx.state.Host, + } + + case ui.SettingsActionSaved, ui.SettingsActionBack: + return popOrExit(ctx.stack) + } + + return router.ScreenExit, nil +} + +func transitionGeneralSettings(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.GeneralSettingsOutput) + if r.Config != nil { + ctx.state.Config = r.Config + } + return popOrExit(ctx.stack) +} + +func transitionCollectionsSettings(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.CollectionsSettingsOutput) + if r.SyncNeeded && ctx.state.CacheSync != nil { + ctx.state.CacheSync.SyncCollections() + } + ctx.showCollections = ctx.state.Config.ShowCollections(ctx.state.Host) + return popOrExit(ctx.stack) +} + +func transitionAdvancedSettings(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.AdvancedSettingsOutput) + + pushInput := ui.AdvancedSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host} + + switch r.Action { + case ui.AdvancedSettingsActionRebuildCache: + ctx.stack.Push(ScreenAdvancedSettings, pushInput, r) + return ScreenRebuildCache, ui.RebuildCacheInput{ + Host: ctx.state.Host, + Config: ctx.state.Config, + } + + case ui.AdvancedSettingsActionSyncArtwork: + ctx.stack.Push(ScreenAdvancedSettings, pushInput, r) + return ScreenArtworkSync, ui.ArtworkSyncInput{ + Config: *ctx.state.Config, + Host: ctx.state.Host, + } + + default: + if ctx.state.AutoUpdate != nil { + ctx.state.AutoUpdate.Recheck(ctx.state.Config.ReleaseChannel) + } + return popOrExit(ctx.stack) + } +} + +func transitionPlatformMapping(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.PlatformMappingOutput) + if r.Action == ui.PlatformMappingActionSaved { + handlePlatformMappingUpdateUI(ctx.state, r) + } + return popOrExit(ctx.stack) +} + +func transitionSaveSyncSettings(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.SaveSyncSettingsOutput) + if r.Config != nil { + ctx.state.Config = r.Config + } + triggerAutoSyncRouter(ctx.state) + return popOrExit(ctx.stack) +} + +func transitionInfo(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.InfoOutput) + if r.Action == ui.InfoActionLogout { + ctx.stack.Push(ScreenInfo, ui.InfoInput{Host: ctx.state.Host}, nil) + return ScreenLogoutConfirmation, nil + } + return popOrExit(ctx.stack) +} + +func transitionLogoutConfirmation(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.LogoutConfirmationOutput) + if r.Action == ui.LogoutConfirmationActionConfirm { + handleLogout(ctx.state) + ctx.stack.Clear() + return ScreenPlatformSelection, ui.PlatformSelectionInput{ + Platforms: ctx.state.Platforms, + QuitOnBack: ctx.quitOnBack, + ShowCollections: ctx.state.Config.ShowCollections(ctx.state.Host), + ShowSaveSync: computeShowSaveSync(ctx.state), + } + } + return popOrExit(ctx.stack) +} + +func transitionRebuildCache(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.RebuildCacheOutput) + if len(r.UpdatedPlatforms) > 0 { + ctx.state.Platforms = r.UpdatedPlatforms + } + return popOrExit(ctx.stack) +} + +func transitionUpdateCheck(ctx *transitionContext, result any) (router.Screen, any) { + r := result.(ui.UpdateOutput) + if r.UpdatePerformed { + os.Exit(0) + } + return popOrExit(ctx.stack) +} + +func popOrExit(stack *router.Stack) (router.Screen, any) { + entry := stack.Pop() + if entry == nil { + return router.ScreenExit, nil + } + + switch input := entry.Input.(type) { + case ui.PlatformSelectionInput: + if entry.Resume != nil { + output := entry.Resume.(ui.PlatformSelectionOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastSelectedPosition = output.LastSelectedPosition + } + return entry.Screen, input + + case ui.GameListInput: + if entry.Resume != nil { + output := entry.Resume.(ui.GameListOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastSelectedPosition = output.LastSelectedPosition + } + return entry.Screen, input + + case ui.CollectionSelectionInput: + if entry.Resume != nil { + output := entry.Resume.(ui.CollectionSelectionOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastSelectedPosition = output.LastSelectedPosition + } + return entry.Screen, input + + case ui.CollectionPlatformSelectionInput: + if entry.Resume != nil { + output := entry.Resume.(ui.CollectionPlatformSelectionOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastSelectedPosition = output.LastSelectedPosition + } + return entry.Screen, input + + case ui.SettingsInput: + if entry.Resume != nil { + output := entry.Resume.(ui.SettingsOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastVisibleStartIndex = output.LastVisibleStartIndex + } + return entry.Screen, input + + case ui.AdvancedSettingsInput: + if entry.Resume != nil { + output := entry.Resume.(ui.AdvancedSettingsOutput) + input.LastSelectedIndex = output.LastSelectedIndex + input.LastVisibleStartIndex = output.LastVisibleStartIndex + } + return entry.Screen, input + + default: + return entry.Screen, entry.Input + } +} diff --git a/cfw/muos/input_mapping.go b/cfw/muos/input_mapping.go index bb37bf4..6e706b9 100644 --- a/cfw/muos/input_mapping.go +++ b/cfw/muos/input_mapping.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" ) @@ -32,11 +31,9 @@ func DetectDevice() Device { output, err := cmd.Output() if err != nil || len(output) == 0 { - logger.Info("Device detection result", "device", DeviceAnbernic, "reason", "TRIMUI not found in input devices") return DeviceAnbernic } - logger.Info("Device detection result", "device", DeviceTrimui, "match", strings.TrimSpace(string(output))) return DeviceTrimui } diff --git a/go.mod b/go.mod index 24c600e..b28d8a1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.2 require ( github.com/BrandonKowalski/certifiable v1.3.0 - github.com/BrandonKowalski/gabagool/v2 v2.6.4 + github.com/BrandonKowalski/gabagool/v2 v2.7.0 github.com/beevik/etree v1.6.0 github.com/bodgit/sevenzip v1.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1 @@ -13,7 +13,7 @@ require ( go.uber.org/atomic v1.11.0 golang.org/x/image v0.35.0 golang.org/x/text v0.33.0 - modernc.org/sqlite v1.44.2 + modernc.org/sqlite v1.44.3 ) require ( diff --git a/go.sum b/go.sum index edf27df..25e75e0 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/BrandonKowalski/gabagool/v2 v2.6.3 h1:eYnPdDAzMU4bijIWdFUEjFNbGoQV5+M github.com/BrandonKowalski/gabagool/v2 v2.6.3/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= github.com/BrandonKowalski/gabagool/v2 v2.6.4 h1:yMvcDFgNnIBpuKnLuAllZpNX0dMYijybgPMDsDCv50U= github.com/BrandonKowalski/gabagool/v2 v2.6.4/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= +github.com/BrandonKowalski/gabagool/v2 v2.7.0 h1:M6WfMTCDtdTa6B3vPlhTB+3GlE9bpBd3gfCWIa0ncJ4= +github.com/BrandonKowalski/gabagool/v2 v2.7.0/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -344,6 +346,8 @@ modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA= modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= modernc.org/sqlite v1.44.2 h1:EdYqXeBpKFJjg8QYnw6E71MpANkoxyuYi+g68ugOL8g= modernc.org/sqlite v1.44.2/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/config.go b/internal/config.go index 01b9630..51c5848 100644 --- a/internal/config.go +++ b/internal/config.go @@ -14,20 +14,12 @@ import ( "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" ) -type ReleaseChannel string - -const ( - ReleaseChannelMatchRomM ReleaseChannel = "match_romm" - ReleaseChannelStable ReleaseChannel = "stable" - ReleaseChannelBeta ReleaseChannel = "beta" -) - var kidModeEnabled atomic.Bool type Config struct { Hosts []romm.Host `json:"hosts,omitempty"` DirectoryMappings map[string]DirectoryMapping `json:"directory_mappings,omitempty"` - SaveSyncMode string `json:"save_sync_mode"` + SaveSyncMode SaveSyncMode `json:"save_sync_mode"` SaveDirectoryMappings map[string]string `json:"save_directory_mappings,omitempty"` GameSaveOverrides map[int]string `json:"game_save_overrides,omitempty"` DownloadArt bool `json:"download_art,omitempty"` @@ -36,12 +28,12 @@ type Config struct { ShowRegularCollections bool `json:"show_collections"` ShowSmartCollections bool `json:"show_smart_collections"` ShowVirtualCollections bool `json:"show_virtual_collections"` - DownloadedGames string `json:"downloaded_games,omitempty"` + DownloadedGames DownloadedGamesMode `json:"downloaded_games,omitempty"` ApiTimeout time.Duration `json:"api_timeout"` DownloadTimeout time.Duration `json:"download_timeout"` - LogLevel string `json:"log_level,omitempty"` + LogLevel LogLevel `json:"log_level,omitempty"` Language string `json:"language,omitempty"` - CollectionView string `json:"collection_view,omitempty"` + CollectionView CollectionView `json:"collection_view,omitempty"` KidMode bool `json:"kid_mode,omitempty"` ReleaseChannel ReleaseChannel `json:"release_channel,omitempty"` @@ -103,15 +95,15 @@ func LoadConfig() (*Config, error) { } if config.DownloadedGames == "" { - config.DownloadedGames = "do_nothing" + config.DownloadedGames = DownloadedGamesModeDoNothing } if config.CollectionView == "" { - config.CollectionView = "platform" + config.CollectionView = CollectionViewPlatform } if config.SaveSyncMode == "" { - config.SaveSyncMode = "off" + config.SaveSyncMode = SaveSyncModeOff } return &config, nil @@ -119,7 +111,7 @@ func LoadConfig() (*Config, error) { func SaveConfig(config *Config) error { if config.LogLevel == "" { - config.LogLevel = "ERROR" + config.LogLevel = LogLevelError } if config.Language == "" { @@ -127,22 +119,22 @@ func SaveConfig(config *Config) error { } if config.DownloadedGames == "" { - config.DownloadedGames = "do_nothing" + config.DownloadedGames = DownloadedGamesModeDoNothing } if config.CollectionView == "" { - config.CollectionView = "platform" + config.CollectionView = CollectionViewPlatform } if config.SaveSyncMode == "" { - config.SaveSyncMode = "off" + config.SaveSyncMode = SaveSyncModeOff } if config.ReleaseChannel == "" { config.ReleaseChannel = ReleaseChannelMatchRomM } - gaba.SetRawLogLevel(config.LogLevel) + gaba.SetRawLogLevel(string(config.LogLevel)) if err := i18n.SetWithCode(config.Language); err != nil { gaba.GetLogger().Error("Failed to set language", "error", err, "language", config.Language) @@ -162,76 +154,6 @@ func SaveConfig(config *Config) error { return nil } -// SortPlatformsByOrder sorts platforms based on the saved order in config. -// If no order is saved, platforms are sorted alphabetically. -func SortPlatformsByOrder(platforms []romm.Platform, order []string) []romm.Platform { - if len(order) == 0 { - // No saved order, return alphabetically sorted - return SortPlatformsAlphabetically(platforms) - } - - // Create a map of fs_slug to platform for quick lookup - platformMap := make(map[string]romm.Platform) - for _, p := range platforms { - platformMap[p.FSSlug] = p - } - - // Create result slice with platforms in saved order - var result []romm.Platform - usedSlugs := make(map[string]bool) - - // Add platforms in saved order - for _, fsSlug := range order { - if platform, exists := platformMap[fsSlug]; exists { - result = append(result, platform) - usedSlugs[fsSlug] = true - } - } - - // Add any new platforms that aren't in the saved order (alphabetically) - var newPlatforms []romm.Platform - for _, p := range platforms { - if !usedSlugs[p.FSSlug] { - newPlatforms = append(newPlatforms, p) - } - } - newPlatforms = SortPlatformsAlphabetically(newPlatforms) - result = append(result, newPlatforms...) - - return result -} - -// SortPlatformsAlphabetically sorts platforms by name -func SortPlatformsAlphabetically(platforms []romm.Platform) []romm.Platform { - sorted := make([]romm.Platform, len(platforms)) - copy(sorted, platforms) - - for i := 0; i < len(sorted); i++ { - for j := i + 1; j < len(sorted); j++ { - if sorted[i].Name > sorted[j].Name { - sorted[i], sorted[j] = sorted[j], sorted[i] - } - } - } - - return sorted -} - -func PrunePlatformOrder(order []string, mappings map[string]DirectoryMapping) []string { - if len(order) == 0 { - return order - } - - pruned := make([]string, 0, len(order)) - for _, fsSlug := range order { - if _, exists := mappings[fsSlug]; exists { - pruned = append(pruned, fsSlug) - } - } - - return pruned -} - func InitKidMode(config *Config) { kidModeEnabled.Store(config.KidMode) } @@ -244,35 +166,6 @@ func SetKidMode(enabled bool) { kidModeEnabled.Store(enabled) } -func GetMappedPlatforms(host romm.Host, mappings map[string]DirectoryMapping, timeout ...time.Duration) ([]romm.Platform, error) { - var rommPlatforms []romm.Platform - var err error - - if cm := cache.GetCacheManager(); cm != nil { - rommPlatforms, err = cm.GetPlatforms() - } - if len(rommPlatforms) == 0 { - c := romm.NewClientFromHost(host, timeout...) - rommPlatforms, err = c.GetPlatforms() - if err != nil { - return nil, fmt.Errorf("failed to get platforms from RomM: %w", err) - } - } - - romm.DisambiguatePlatformNames(rommPlatforms) - - var platforms []romm.Platform - - for _, platform := range rommPlatforms { - _, exists := mappings[platform.FSSlug] - if exists { - platforms = append(platforms, platform) - } - } - - return platforms, nil -} - // LoadPlatformsBinding fetches the PLATFORMS_BINDING from the RomM server // and stores it in the config for use in CFW lookups. // This requires the pointer receiver! diff --git a/internal/constants.go b/internal/constants.go new file mode 100644 index 0000000..4f8f4b6 --- /dev/null +++ b/internal/constants.go @@ -0,0 +1,15 @@ +package internal + +import "time" + +const ( + MultipleFilesIcon = "\U000F0222" + MultipleDownloadedIcon = "\U000F09E9" +) + +const ( + DefaultHTTPTimeout = 10 * time.Second + UpdaterTimeout = 10 * time.Minute + LoginTimeout = 6 * time.Second + ValidationTimeout = 3 * time.Second +) diff --git a/internal/constants/exit_codes.go b/internal/constants/exit_codes.go deleted file mode 100644 index 2cffaa8..0000000 --- a/internal/constants/exit_codes.go +++ /dev/null @@ -1,29 +0,0 @@ -package constants - -import ( - gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" -) - -const ( - ExitCodeEditMappings gaba.ExitCode = 100 - ExitCodeSaveSync gaba.ExitCode = 101 - ExitCodeInfo gaba.ExitCode = 102 - ExitCodeLogout gaba.ExitCode = 103 - ExitCodeBIOS gaba.ExitCode = 104 - ExitCodeLogoutConfirm gaba.ExitCode = 105 - ExitCodeSyncArtwork gaba.ExitCode = 106 - ExitCodeCollectionsSettings gaba.ExitCode = 107 - ExitCodeAdvancedSettings gaba.ExitCode = 108 - ExitCodeRebuildCache gaba.ExitCode = 111 - ExitCodeSaveSyncSettings gaba.ExitCode = 112 - ExitCodeGameOptions gaba.ExitCode = 113 - ExitCodeGeneralSettings gaba.ExitCode = 114 - ExitCodeCheckUpdate gaba.ExitCode = 115 - ExitCodeDownloadRequested gaba.ExitCode = 116 - ExitCodeSearch gaba.ExitCode = 200 - ExitCodeClearSearch gaba.ExitCode = 201 - ExitCodeCollections gaba.ExitCode = 300 - ExitCodeBackToCollection gaba.ExitCode = 301 - ExitCodeBackToCollectionPlatform gaba.ExitCode = 302 - ExitCodeNoResults gaba.ExitCode = 404 -) diff --git a/internal/constants/icons.go b/internal/constants/icons.go deleted file mode 100644 index 1c8be3f..0000000 --- a/internal/constants/icons.go +++ /dev/null @@ -1,4 +0,0 @@ -package constants - -const MultipleFiles = "\U000F0222" -const MultipleDownloaded = "\U000F09E9" diff --git a/internal/constants/timeouts.go b/internal/constants/timeouts.go deleted file mode 100644 index e796dcf..0000000 --- a/internal/constants/timeouts.go +++ /dev/null @@ -1,10 +0,0 @@ -package constants - -import "time" - -const ( - DefaultHTTPTimeout = 10 * time.Second - UpdaterTimeout = 10 * time.Minute - LoginTimeout = 6 * time.Second // Timeout for login attempts - ValidationTimeout = 3 * time.Second // Fast timeout for pre-flight connection checks -) diff --git a/internal/enums.go b/internal/enums.go new file mode 100644 index 0000000..8724f9c --- /dev/null +++ b/internal/enums.go @@ -0,0 +1,40 @@ +package internal + +type ReleaseChannel string + +const ( + ReleaseChannelMatchRomM ReleaseChannel = "match_romm" + ReleaseChannelStable ReleaseChannel = "stable" + ReleaseChannelBeta ReleaseChannel = "beta" +) + +type SaveSyncMode string + +const ( + SaveSyncModeOff SaveSyncMode = "off" + SaveSyncModeManual SaveSyncMode = "manual" + SaveSyncModeAutomatic SaveSyncMode = "automatic" +) + +type DownloadedGamesMode string + +const ( + DownloadedGamesModeDoNothing DownloadedGamesMode = "do_nothing" + DownloadedGamesModeMark DownloadedGamesMode = "mark" + DownloadedGamesModeFilter DownloadedGamesMode = "filter" +) + +type CollectionView string + +const ( + CollectionViewPlatform CollectionView = "platform" + CollectionViewUnified CollectionView = "unified" +) + +type LogLevel string + +const ( + LogLevelDebug LogLevel = "DEBUG" + LogLevelInfo LogLevel = "INFO" + LogLevelError LogLevel = "ERROR" +) diff --git a/internal/environment/variables.go b/internal/environment/environment.go similarity index 100% rename from internal/environment/variables.go rename to internal/environment/environment.go diff --git a/internal/jsonutil/loader.go b/internal/jsonutil/jsonutil.go similarity index 90% rename from internal/jsonutil/loader.go rename to internal/jsonutil/jsonutil.go index 54bb6ad..be67601 100644 --- a/internal/jsonutil/loader.go +++ b/internal/jsonutil/jsonutil.go @@ -12,12 +12,10 @@ func LoadJSONMap[K comparable, V any](fs embed.FS, path string, overridePrefix s var data []byte var err error - // Check for override file in current working directory overridePath := filepath.Join("overrides", overridePrefix, path) if fileData, readErr := os.ReadFile(overridePath); readErr == nil { data = fileData } else { - // Fall back to embedded file data, err = fs.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", path, err) diff --git a/internal/platforms.go b/internal/platforms.go new file mode 100644 index 0000000..cf22af4 --- /dev/null +++ b/internal/platforms.go @@ -0,0 +1,102 @@ +package internal + +import ( + "fmt" + "grout/cache" + "grout/romm" + "time" +) + +// SortPlatformsByOrder sorts platforms based on the saved order. +// If no order is saved, platforms are sorted alphabetically. +func SortPlatformsByOrder(platforms []romm.Platform, order []string) []romm.Platform { + if len(order) == 0 { + return SortPlatformsAlphabetically(platforms) + } + + platformMap := make(map[string]romm.Platform) + for _, p := range platforms { + platformMap[p.FSSlug] = p + } + + var result []romm.Platform + usedSlugs := make(map[string]bool) + + for _, fsSlug := range order { + if platform, exists := platformMap[fsSlug]; exists { + result = append(result, platform) + usedSlugs[fsSlug] = true + } + } + + var newPlatforms []romm.Platform + for _, p := range platforms { + if !usedSlugs[p.FSSlug] { + newPlatforms = append(newPlatforms, p) + } + } + newPlatforms = SortPlatformsAlphabetically(newPlatforms) + result = append(result, newPlatforms...) + + return result +} + +// SortPlatformsAlphabetically sorts platforms by name +func SortPlatformsAlphabetically(platforms []romm.Platform) []romm.Platform { + sorted := make([]romm.Platform, len(platforms)) + copy(sorted, platforms) + + for i := 0; i < len(sorted); i++ { + for j := i + 1; j < len(sorted); j++ { + if sorted[i].Name > sorted[j].Name { + sorted[i], sorted[j] = sorted[j], sorted[i] + } + } + } + + return sorted +} + +func PrunePlatformOrder(order []string, mappings map[string]DirectoryMapping) []string { + if len(order) == 0 { + return order + } + + pruned := make([]string, 0, len(order)) + for _, fsSlug := range order { + if _, exists := mappings[fsSlug]; exists { + pruned = append(pruned, fsSlug) + } + } + + return pruned +} + +func GetMappedPlatforms(host romm.Host, mappings map[string]DirectoryMapping, timeout ...time.Duration) ([]romm.Platform, error) { + var rommPlatforms []romm.Platform + var err error + + if cm := cache.GetCacheManager(); cm != nil { + rommPlatforms, err = cm.GetPlatforms() + } + if len(rommPlatforms) == 0 { + c := romm.NewClientFromHost(host, timeout...) + rommPlatforms, err = c.GetPlatforms() + if err != nil { + return nil, fmt.Errorf("failed to get platforms from RomM: %w", err) + } + } + + romm.DisambiguatePlatformNames(rommPlatforms) + + var platforms []romm.Platform + + for _, platform := range rommPlatforms { + _, exists := mappings[platform.FSSlug] + if exists { + platforms = append(platforms, platform) + } + } + + return platforms, nil +} diff --git a/ui/actions.go b/ui/actions.go new file mode 100644 index 0000000..c6b97e8 --- /dev/null +++ b/ui/actions.go @@ -0,0 +1,154 @@ +package ui + +// Action types for each screen. +// Screens set these directly in their output, eliminating the need for exit code mapping. + +type PlatformSelectionAction int + +const ( + PlatformSelectionActionSelected PlatformSelectionAction = iota + PlatformSelectionActionCollections + PlatformSelectionActionSettings + PlatformSelectionActionSaveSync + PlatformSelectionActionQuit +) + +type GameListAction int + +const ( + GameListActionSelected GameListAction = iota + GameListActionSearch + GameListActionBIOS + GameListActionBack + GameListActionClearSearch +) + +type GameDetailsAction int + +const ( + GameDetailsActionDownload GameDetailsAction = iota + GameDetailsActionOptions + GameDetailsActionBack +) + +type GameOptionsAction int + +const ( + GameOptionsActionSaved GameOptionsAction = iota + GameOptionsActionBack +) + +type SearchAction int + +const ( + SearchActionApply SearchAction = iota + SearchActionCancel +) + +type CollectionListAction int + +const ( + CollectionListActionSelected CollectionListAction = iota + CollectionListActionSearch + CollectionListActionClearSearch + CollectionListActionBack +) + +type CollectionPlatformSelectionAction int + +const ( + CollectionPlatformSelectionActionSelected CollectionPlatformSelectionAction = iota + CollectionPlatformSelectionActionBack +) + +type SettingsAction int + +const ( + SettingsActionSaved SettingsAction = iota + SettingsActionGeneral + SettingsActionCollections + SettingsActionAdvanced + SettingsActionPlatformMapping + SettingsActionSaveSync + SettingsActionInfo + SettingsActionCheckUpdate + SettingsActionBack +) + +type GeneralSettingsAction int + +const ( + GeneralSettingsActionSaved GeneralSettingsAction = iota + GeneralSettingsActionBack +) + +type CollectionsSettingsAction int + +const ( + CollectionsSettingsActionSaved CollectionsSettingsAction = iota + CollectionsSettingsActionBack +) + +type AdvancedSettingsAction int + +const ( + AdvancedSettingsActionSaved AdvancedSettingsAction = iota + AdvancedSettingsActionRebuildCache + AdvancedSettingsActionSyncArtwork + AdvancedSettingsActionBack +) + +type PlatformMappingAction int + +const ( + PlatformMappingActionSaved PlatformMappingAction = iota + PlatformMappingActionBack +) + +type SaveSyncSettingsAction int + +const ( + SaveSyncSettingsActionSaved SaveSyncSettingsAction = iota + SaveSyncSettingsActionBack +) + +type InfoAction int + +const ( + InfoActionLogout InfoAction = iota + InfoActionBack +) + +type LogoutConfirmationAction int + +const ( + LogoutConfirmationActionConfirm LogoutConfirmationAction = iota + LogoutConfirmationActionCancel +) + +type SaveSyncAction int + +const ( + SaveSyncActionComplete SaveSyncAction = iota + SaveSyncActionBack +) + +type BIOSDownloadAction int + +const ( + BIOSDownloadActionComplete BIOSDownloadAction = iota + BIOSDownloadActionBack +) + +type ArtworkSyncAction int + +const ( + ArtworkSyncActionComplete ArtworkSyncAction = iota +) + +type UpdateCheckAction int + +const ( + UpdateCheckActionComplete UpdateCheckAction = iota + UpdateCheckActionBack +) diff --git a/ui/advanced_settings.go b/ui/advanced_settings.go index d0a17c5..a6d1d5f 100644 --- a/ui/advanced_settings.go +++ b/ui/advanced_settings.go @@ -3,7 +3,6 @@ package ui import ( "errors" "grout/internal" - "grout/internal/constants" "grout/romm" "time" @@ -20,6 +19,7 @@ type AdvancedSettingsInput struct { } type AdvancedSettingsOutput struct { + Action AdvancedSettingsAction RebuildCacheClicked bool SyncArtworkClicked bool LastSelectedIndex int @@ -32,9 +32,9 @@ func NewAdvancedSettingsScreen() *AdvancedSettingsScreen { return &AdvancedSettingsScreen{} } -func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult[AdvancedSettingsOutput], error) { +func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (AdvancedSettingsOutput, error) { config := input.Config - output := AdvancedSettingsOutput{} + output := AdvancedSettingsOutput{Action: AdvancedSettingsActionBack} items := s.buildMenuItems(config) @@ -49,7 +49,7 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult InitialSelectedIndex: input.LastSelectedIndex, VisibleStartIndex: input.LastVisibleStartIndex, StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) @@ -61,10 +61,10 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("Advanced settings error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } if result.Action == gaba.ListActionSelected { @@ -72,12 +72,14 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_rebuild_cache", Other: "Rebuild Cache"}, nil) { output.RebuildCacheClicked = true - return withCode(output, constants.ExitCodeRebuildCache), nil + output.Action = AdvancedSettingsActionRebuildCache + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_sync_artwork", Other: "Preload Artwork"}, nil) { output.SyncArtworkClicked = true - return withCode(output, constants.ExitCodeSyncArtwork), nil + output.Action = AdvancedSettingsActionSyncArtwork + return output, nil } } @@ -86,10 +88,11 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult err = internal.SaveConfig(config) if err != nil { gaba.GetLogger().Error("Error saving advanced settings", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = AdvancedSettingsActionSaved + return output, nil } func (s *AdvancedSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions { @@ -152,9 +155,9 @@ func (s *AdvancedSettingsScreen) buildMenuItems(config *internal.Config) []gaba. { Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_log_level", Other: "Log Level"}, nil)}, Options: []gaba.Option{ - {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_debug", Other: "Debug"}, nil), Value: "DEBUG"}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_info", Other: "Info"}, nil), Value: "INFO"}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_error", Other: "Error"}, nil), Value: "ERROR"}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_debug", Other: "Debug"}, nil), Value: internal.LogLevelDebug}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_info", Other: "Info"}, nil), Value: internal.LogLevelInfo}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "log_level_error", Other: "Error"}, nil), Value: internal.LogLevelError}, }, SelectedOption: logLevelToIndex(config.LogLevel), }, @@ -177,7 +180,7 @@ func (s *AdvancedSettingsScreen) applySettings(config *internal.Config, items [] } case i18n.Localize(&goi18n.Message{ID: "settings_log_level", Other: "Log Level"}, nil): - if val, ok := item.Options[item.SelectedOption].Value.(string); ok { + if val, ok := item.Options[item.SelectedOption].Value.(internal.LogLevel); ok { config.LogLevel = val } diff --git a/ui/artwork_sync.go b/ui/artwork_sync.go index 6cc328e..bc3c315 100644 --- a/ui/artwork_sync.go +++ b/ui/artwork_sync.go @@ -171,7 +171,7 @@ func (s *ArtworkSyncScreen) draw(input ArtworkSyncInput) { headers["Authorization"] = input.Host.BasicAuthHeader() res, err := gaba.DownloadManager(downloads, headers, gaba.DownloadManagerOptions{ - AutoContinue: true, + AutoContinueOnComplete: true, }) if err != nil { logger.Error("Artwork download failed", "error", err) diff --git a/ui/bios_download.go b/ui/bios_download.go index 4afae65..664d88c 100644 --- a/ui/bios_download.go +++ b/ui/bios_download.go @@ -45,10 +45,10 @@ func (s *BIOSDownloadScreen) Execute(config internal.Config, host romm.Host, pla return BIOSDownloadOutput{Platform: platform} } - return result.Value + return result } -func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDownloadOutput], error) { +func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (BIOSDownloadOutput, error) { logger := gaba.GetLogger() output := BIOSDownloadOutput{ @@ -65,7 +65,7 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow ContinueFooter(), gaba.MessageOptions{}, ) - return back(output), nil + return output, nil } if len(firmwareList) == 0 { @@ -75,7 +75,7 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow ContinueFooter(), gaba.MessageOptions{}, ) - return back(output), nil + return output, nil } logger.Debug("Fetched firmware from RomM", "count", len(firmwareList), "platform_id", input.Platform.ID) @@ -192,8 +192,8 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow } options := gaba.DefaultListOptions(fmt.Sprintf("%s - BIOS", input.Platform.Name), menuItems) - options.SmallTitle = true - options.StartInMultiSelectMode = true + options.UseSmallTitle = true + options.InitialMultiSelectMode = true options.FooterHelpItems = []gaba.FooterHelpItem{ FooterBack(), {ButtonName: icons.Start, HelpText: i18n.Localize(&goi18n.Message{ID: "button_download", Other: "Download"}, nil), IsConfirmButton: true}, @@ -203,11 +203,11 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow sel, err := gaba.List(options) if err != nil { logger.Error("BIOS selection failed", "error", err) - return back(output), err + return output, err } if sel.Action != gaba.ListActionSelected || len(sel.Selected) == 0 { - return back(output), nil + return output, nil } var selectedItems []firmwareWithMetadata @@ -245,11 +245,11 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow headers["Authorization"] = input.Host.BasicAuthHeader() res, err := gaba.DownloadManager(downloads, headers, gaba.DownloadManagerOptions{ - AutoContinue: true, + AutoContinueOnComplete: true, }) if err != nil { logger.Error("BIOS download failed", "error", err) - return back(output), err + return output, err } logger.Debug("Download results", "completed", len(res.Completed), "failed", len(res.Failed)) @@ -330,5 +330,5 @@ func (s *BIOSDownloadScreen) draw(input BIOSDownloadInput) (ScreenResult[BIOSDow ) } - return back(output), nil + return output, nil } diff --git a/ui/collection_platform_selection.go b/ui/collection_platform_selection.go index e806557..b6881d1 100644 --- a/ui/collection_platform_selection.go +++ b/ui/collection_platform_selection.go @@ -25,6 +25,7 @@ type CollectionPlatformSelectionInput struct { } type CollectionPlatformSelectionOutput struct { + Action CollectionPlatformSelectionAction SelectedPlatform romm.Platform Collection romm.Collection AllGames []romm.Rom @@ -38,9 +39,10 @@ func NewCollectionPlatformSelectionScreen() *CollectionPlatformSelectionScreen { return &CollectionPlatformSelectionScreen{} } -func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelectionInput) (ScreenResult[CollectionPlatformSelectionOutput], error) { +func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelectionInput) (CollectionPlatformSelectionOutput, error) { logger := gaba.GetLogger() output := CollectionPlatformSelectionOutput{ + Action: CollectionPlatformSelectionActionBack, Collection: input.Collection, LastSelectedIndex: input.LastSelectedIndex, LastSelectedPosition: input.LastSelectedPosition, @@ -76,12 +78,12 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti return nil, nil }, ) - return withCode(output, gaba.ExitCodeBack), nil + return output, nil } } // Handle unified mode - skip platform selection and return all games - if input.Config.CollectionView == "unified" { + if input.Config.CollectionView == internal.CollectionViewUnified { // Filter games to only include those with mapped platforms filteredGames := make([]romm.Rom, 0) for _, game := range allGames { @@ -92,7 +94,8 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti output.AllGames = filteredGames output.SelectedPlatform = romm.Platform{ID: 0} // ID=0 signals unified mode - return success(output), nil + output.Action = CollectionPlatformSelectionActionSelected + return output, nil } platformMap := make(map[int]romm.Platform) @@ -117,7 +120,7 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti return nil, nil }, ) - return withCode(output, gaba.ExitCodeBack), nil + return output, nil } platforms := make([]romm.Platform, 0, len(platformMap)) @@ -155,7 +158,7 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti title := i18n.Localize(&goi18n.Message{ID: "collection_platform_title", Other: "{{.Name}} - Platforms"}, map[string]interface{}{"Name": input.Collection.Name}) options := gaba.DefaultListOptions(title, menuItems) - options.SmallTitle = true + options.UseSmallTitle = true options.FooterHelpItems = footerItems options.SelectedIndex = input.LastSelectedIndex options.VisibleStartIndex = max(0, input.LastSelectedIndex-input.LastSelectedPosition) @@ -164,9 +167,9 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti sel, err := gaba.List(options) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } switch sel.Action { @@ -177,9 +180,10 @@ func (s *CollectionPlatformSelectionScreen) Draw(input CollectionPlatformSelecti output.AllGames = allGames output.LastSelectedIndex = sel.Selected[0] output.LastSelectedPosition = sel.VisiblePosition - return success(output), nil + output.Action = CollectionPlatformSelectionActionSelected + return output, nil default: - return withCode(output, gaba.ExitCodeBack), nil + return output, nil } } diff --git a/ui/collection_selection.go b/ui/collection_selection.go index 6ec9e5d..eb8658a 100644 --- a/ui/collection_selection.go +++ b/ui/collection_selection.go @@ -5,7 +5,6 @@ import ( "fmt" "grout/cache" "grout/internal" - "grout/internal/constants" "grout/romm" "slices" "strings" @@ -25,6 +24,7 @@ type CollectionSelectionInput struct { } type CollectionSelectionOutput struct { + Action CollectionListAction SelectedCollection romm.Collection SearchFilter string LastSelectedIndex int @@ -37,8 +37,9 @@ func NewCollectionSelectionScreen() *CollectionSelectionScreen { return &CollectionSelectionScreen{} } -func (s *CollectionSelectionScreen) Draw(input CollectionSelectionInput) (ScreenResult[CollectionSelectionOutput], error) { +func (s *CollectionSelectionScreen) Draw(input CollectionSelectionInput) (CollectionSelectionOutput, error) { output := CollectionSelectionOutput{ + Action: CollectionListActionBack, SearchFilter: input.SearchFilter, LastSelectedIndex: input.LastSelectedIndex, LastSelectedPosition: input.LastSelectedPosition, @@ -100,7 +101,7 @@ func (s *CollectionSelectionScreen) Draw(input CollectionSelectionInput) (Screen } if len(displayCollections) == 0 { - return withCode(output, gaba.ExitCode(404)), nil + return output, nil } var menuItems []gaba.MenuItem @@ -138,11 +139,12 @@ func (s *CollectionSelectionScreen) Draw(input CollectionSelectionInput) (Screen output.SearchFilter = "" output.LastSelectedIndex = 0 output.LastSelectedPosition = 0 - return withCode(output, constants.ExitCodeClearSearch), nil + output.Action = CollectionListActionClearSearch + return output, nil } - return back(output), nil + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } switch sel.Action { @@ -152,12 +154,14 @@ func (s *CollectionSelectionScreen) Draw(input CollectionSelectionInput) (Screen output.SelectedCollection = collection output.LastSelectedIndex = sel.Selected[0] output.LastSelectedPosition = sel.VisiblePosition - return success(output), nil + output.Action = CollectionListActionSelected + return output, nil case gaba.ListActionTriggered: - return withCode(output, constants.ExitCodeSearch), nil + output.Action = CollectionListActionSearch + return output, nil default: - return withCode(output, gaba.ExitCodeBack), nil + return output, nil } } diff --git a/ui/collections_settings.go b/ui/collections_settings.go index 0dd4504..446c14a 100644 --- a/ui/collections_settings.go +++ b/ui/collections_settings.go @@ -14,6 +14,7 @@ type CollectionsSettingsInput struct { } type CollectionsSettingsOutput struct { + Action CollectionsSettingsAction SyncNeeded bool } @@ -23,9 +24,9 @@ func NewCollectionsSettingsScreen() *CollectionsSettingsScreen { return &CollectionsSettingsScreen{} } -func (s *CollectionsSettingsScreen) Draw(input CollectionsSettingsInput) (ScreenResult[CollectionsSettingsOutput], error) { +func (s *CollectionsSettingsScreen) Draw(input CollectionsSettingsInput) (CollectionsSettingsOutput, error) { config := input.Config - output := CollectionsSettingsOutput{} + output := CollectionsSettingsOutput{Action: CollectionsSettingsActionBack} prevRegular := config.ShowRegularCollections prevSmart := config.ShowSmartCollections @@ -43,17 +44,17 @@ func (s *CollectionsSettingsScreen) Draw(input CollectionsSettingsInput) (Screen }, InitialSelectedIndex: 0, StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("Collections settings error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } s.applySettings(config, result.Items) @@ -67,10 +68,11 @@ func (s *CollectionsSettingsScreen) Draw(input CollectionsSettingsInput) (Screen err = internal.SaveConfig(config) if err != nil { gaba.GetLogger().Error("Error saving collections settings", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = CollectionsSettingsActionSaved + return output, nil } func (s *CollectionsSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions { @@ -102,8 +104,8 @@ func (s *CollectionsSettingsScreen) buildMenuItems(config *internal.Config) []ga { Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_collection_view", Other: "Collection View"}, nil)}, Options: []gaba.Option{ - {DisplayName: i18n.Localize(&goi18n.Message{ID: "collection_view_platform", Other: "Platform"}, nil), Value: "platform"}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "collection_view_unified", Other: "Unified"}, nil), Value: "unified"}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "collection_view_platform", Other: "Platform"}, nil), Value: internal.CollectionViewPlatform}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "collection_view_unified", Other: "Unified"}, nil), Value: internal.CollectionViewUnified}, }, SelectedOption: collectionViewToIndex(config.CollectionView), }, @@ -131,7 +133,7 @@ func (s *CollectionsSettingsScreen) applySettings(config *internal.Config, items } case i18n.Localize(&goi18n.Message{ID: "settings_collection_view", Other: "Collection View"}, nil): - if val, ok := item.Options[item.SelectedOption].Value.(string); ok { + if val, ok := item.Options[item.SelectedOption].Value.(internal.CollectionView); ok { config.CollectionView = val } } diff --git a/ui/download.go b/ui/download.go index 637996e..7dbb7de 100644 --- a/ui/download.go +++ b/ui/download.go @@ -76,14 +76,14 @@ func (s *DownloadScreen) Execute(config internal.Config, host romm.Host, platfor } } - if result.ExitCode == gaba.ExitCodeSuccess && len(result.Value.DownloadedGames) > 0 { - gaba.GetLogger().Debug("Successfully downloaded games", "count", len(result.Value.DownloadedGames)) + if len(result.DownloadedGames) > 0 { + gaba.GetLogger().Debug("Successfully downloaded games", "count", len(result.DownloadedGames)) } - return result.Value + return result } -func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput], error) { +func (s *DownloadScreen) draw(input downloadInput) (downloadOutput, error) { logger := gaba.GetLogger() output := downloadOutput{ @@ -104,8 +104,8 @@ func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput] logger.Debug("Starting ROM download", "downloads", downloads) res, err := gaba.DownloadManager(downloads, headers, gaba.DownloadManagerOptions{ - AutoContinue: input.Config.DownloadArt, - InsecureSkipVerify: input.Host.InsecureSkipVerify, + AutoContinueOnComplete: input.Config.DownloadArt, + SkipSSLVerification: input.Host.InsecureSkipVerify, }) if err != nil { logger.Error("Error downloading", "error", err) @@ -117,7 +117,7 @@ func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput] } } - return withCode(output, gaba.ExitCodeError), err + return output, err } logger.Debug("Download results", "completed", len(res.Completed), "failed", len(res.Failed)) @@ -138,7 +138,7 @@ func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput] } if len(res.Completed) == 0 { - return withCode(output, gaba.ExitCodeError), nil + return output, nil } for _, g := range input.SelectedGames { @@ -305,7 +305,7 @@ func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput] } output.DownloadedGames = downloadedGames - return success(output), nil + return output, nil } func (s *DownloadScreen) buildDownloads(config internal.Config, host romm.Host, platform romm.Platform, games []romm.Rom, selectedFileID int) ([]gaba.Download, []artDownload) { diff --git a/ui/emulator_selection.go b/ui/emulator_selection.go index f736903..acaf298 100644 --- a/ui/emulator_selection.go +++ b/ui/emulator_selection.go @@ -37,7 +37,7 @@ func NewEmulatorSelectionScreen() *EmulatorSelectionScreen { return &EmulatorSelectionScreen{} } -func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (ScreenResult[EmulatorSelectionOutput], error) { +func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (EmulatorSelectionOutput, error) { output := EmulatorSelectionOutput{ LastSelectedIndex: input.LastSelectedIndex, LastSelectedPosition: input.LastSelectedPosition, @@ -82,7 +82,7 @@ func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (ScreenResu title := i18n.Localize(&goi18n.Message{ID: "emulator_selection_title", Other: "Select {{.Platform}} Emulator"}, map[string]interface{}{"Platform": input.PlatformName}) options := gaba.DefaultListOptions(title, menuItems) - options.SmallTitle = true + options.UseSmallTitle = true options.FooterHelpItems = footerItems options.SelectedIndex = input.LastSelectedIndex options.VisibleStartIndex = max(0, input.LastSelectedIndex-input.LastSelectedPosition) @@ -91,9 +91,9 @@ func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (ScreenResu sel, err := gaba.List(options) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } switch sel.Action { @@ -103,9 +103,9 @@ func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (ScreenResu output.SelectedEmulator = selectedEmulator output.LastSelectedIndex = sel.Selected[0] output.LastSelectedPosition = sel.VisiblePosition - return success(output), nil + return output, nil default: - return back(output), nil + return output, nil } } diff --git a/ui/game_details.go b/ui/game_details.go index 07a0b5c..f7a29b0 100644 --- a/ui/game_details.go +++ b/ui/game_details.go @@ -5,7 +5,6 @@ import ( "fmt" "grout/cache" "grout/internal" - constants2 "grout/internal/constants" "grout/internal/fileutil" "grout/internal/imageutil" "grout/internal/stringutil" @@ -33,6 +32,7 @@ type GameDetailsInput struct { } type GameDetailsOutput struct { + Action GameDetailsAction DownloadRequested bool SelectedFileID int Game romm.Rom @@ -45,9 +45,10 @@ func NewGameDetailsScreen() *GameDetailsScreen { return &GameDetailsScreen{} } -func (s *GameDetailsScreen) Draw(input GameDetailsInput) (ScreenResult[GameDetailsOutput], error) { +func (s *GameDetailsScreen) Draw(input GameDetailsInput) (GameDetailsOutput, error) { logger := gaba.GetLogger() output := GameDetailsOutput{ + Action: GameDetailsActionBack, Game: input.Game, Platform: input.Platform, } @@ -64,7 +65,7 @@ func (s *GameDetailsScreen) Draw(input GameDetailsInput) (ScreenResult[GameDetai } if !internal.IsKidModeEnabled() { options.ActionButton = constants.VirtualButtonY - options.EnableAction = true + options.AllowAction = true } footerItems := []gaba.FooterHelpItem{ @@ -87,13 +88,14 @@ func (s *GameDetailsScreen) Draw(input GameDetailsInput) (ScreenResult[GameDetai if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } logger.Error("Detail screen error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } if result.Action == gaba.DetailActionConfirmed { + output.Action = GameDetailsActionDownload output.DownloadRequested = true // Check if a specific file was selected from the dropdown for _, selection := range result.DropdownSelections { @@ -104,14 +106,15 @@ func (s *GameDetailsScreen) Draw(input GameDetailsInput) (ScreenResult[GameDetai break } } - return withCode(output, constants2.ExitCodeDownloadRequested), nil + return output, nil } if result.Action == gaba.DetailActionTriggered { - return withCode(output, constants2.ExitCodeGameOptions), nil + output.Action = GameDetailsActionOptions + return output, nil } - return back(output), nil + return output, nil } func (s *GameDetailsScreen) buildSections(input GameDetailsInput) []gaba.Section { @@ -301,7 +304,7 @@ func (s *GameDetailsScreen) fetchImageFromURL(host romm.Host, imageURL string) [ req.SetBasicAuth(host.Username, host.Password) - client := &http.Client{Timeout: constants2.DefaultHTTPTimeout} + client := &http.Client{Timeout: internal.DefaultHTTPTimeout} resp, err := client.Do(req) if err != nil { logger.Warn("Failed to fetch image", "url", imageURL, "error", err) diff --git a/ui/game_options.go b/ui/game_options.go index a106da3..48a5214 100644 --- a/ui/game_options.go +++ b/ui/game_options.go @@ -20,6 +20,7 @@ type GameOptionsInput struct { } type GameOptionsOutput struct { + Action GameOptionsAction Config *internal.Config } @@ -29,15 +30,15 @@ func NewGameOptionsScreen() *GameOptionsScreen { return &GameOptionsScreen{} } -func (s *GameOptionsScreen) Draw(input GameOptionsInput) (ScreenResult[GameOptionsOutput], error) { +func (s *GameOptionsScreen) Draw(input GameOptionsInput) (GameOptionsOutput, error) { config := input.Config - output := GameOptionsOutput{Config: config} + output := GameOptionsOutput{Action: GameOptionsActionBack, Config: config} items := s.buildMenuItems(config, input.Game) if len(items) == 0 { gaba.GetLogger().Warn("No options available for game") - return back(output), nil + return output, nil } title := i18n.Localize(&goi18n.Message{ID: "game_options_title", Other: "Game Options"}, nil) @@ -48,17 +49,17 @@ func (s *GameOptionsScreen) Draw(input GameOptionsInput) (ScreenResult[GameOptio FooterHelpItems: OptionsListFooter(), InitialSelectedIndex: 0, StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("Game options screen error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } s.applySettings(config, input.Game, result.Items) @@ -66,10 +67,11 @@ func (s *GameOptionsScreen) Draw(input GameOptionsInput) (ScreenResult[GameOptio err = internal.SaveConfig(config) if err != nil { gaba.GetLogger().Error("Error saving game options", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = GameOptionsActionSaved + return output, nil } func (s *GameOptionsScreen) buildMenuItems(config *internal.Config, game romm.Rom) []gaba.ItemWithOptions { diff --git a/ui/games_list.go b/ui/games_list.go index f430b98..1308eaf 100644 --- a/ui/games_list.go +++ b/ui/games_list.go @@ -5,7 +5,6 @@ import ( "fmt" "grout/cache" "grout/internal" - "grout/internal/constants" "grout/internal/stringutil" "grout/romm" "slices" @@ -40,6 +39,7 @@ type GameListInput struct { } type GameListOutput struct { + Action GameListAction SelectedGames []romm.Rom Platform romm.Platform Collection romm.Collection @@ -60,7 +60,7 @@ func isCollectionSet(c romm.Collection) bool { return c.ID != 0 || c.VirtualID != "" } -func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput], error) { +func (s *GameListScreen) Draw(input GameListInput) (GameListOutput, error) { games := input.Games hasBIOS := input.HasBIOS @@ -68,7 +68,7 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] loaded, err := s.loadGames(input) if err != nil { s.showErrorMessage(err) - return back(GameListOutput{}), nil + return GameListOutput{Action: GameListActionBack}, nil } games = loaded.games hasBIOS = loaded.hasBIOS @@ -79,6 +79,7 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] } output := GameListOutput{ + Action: GameListActionBack, Platform: input.Platform, Collection: input.Collection, SearchFilter: input.SearchFilter, @@ -90,7 +91,7 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] displayGames := stringutil.PrepareRomNames(games) - if input.Config.DownloadedGames == "filter" { + if input.Config.DownloadedGames == internal.DownloadedGamesModeFilter { filteredGames := make([]romm.Rom, 0, len(displayGames)) for _, game := range displayGames { if !game.IsDownloaded(*input.Config) { @@ -118,14 +119,14 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] if input.Platform.ID == 0 { for i := range displayGames { prefix := "" - if input.Config.DownloadedGames == "mark" && displayGames[i].IsDownloaded(*input.Config) { + if input.Config.DownloadedGames == internal.DownloadedGamesModeMark && displayGames[i].IsDownloaded(*input.Config) { prefix = gabaconst.Download + " " } displayGames[i].DisplayName = fmt.Sprintf("%s[%s] %s", prefix, displayGames[i].PlatformFSSlug, displayGames[i].DisplayName) } } else { displayName = fmt.Sprintf("%s - %s", input.Collection.Name, input.Platform.Name) - if input.Config.DownloadedGames == "mark" { + if input.Config.DownloadedGames == internal.DownloadedGamesModeMark { for i := range displayGames { if displayGames[i].IsDownloaded(*input.Config) { displayGames[i].DisplayName = fmt.Sprintf("%s %s", gabaconst.Download, displayGames[i].DisplayName) @@ -150,16 +151,16 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] } } - if input.Config.DownloadedGames == "mark" { + if input.Config.DownloadedGames == internal.DownloadedGamesModeMark { if allDownloaded { - prefix = constants.MultipleDownloaded + " " + prefix = internal.MultipleDownloadedIcon + " " } else if anyDownloaded { prefix = gabaconst.Download + " " } } - prefix += constants.MultipleFiles + " " + prefix += internal.MultipleFilesIcon + " " } else { - if input.Config.DownloadedGames == "mark" && game.IsDownloaded(*input.Config) { + if input.Config.DownloadedGames == internal.DownloadedGamesModeMark && game.IsDownloaded(*input.Config) { prefix = gabaconst.Download + " " } } @@ -184,15 +185,11 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] s.showEmptyMessage(displayName, input.SearchFilter) } if input.SearchFilter != "" { - return withCode(output, constants.ExitCodeNoResults), nil + output.Action = GameListActionSearch + return output, nil } - if isCollectionSet(input.Collection) && input.Platform.ID != 0 { - return withCode(output, constants.ExitCodeBackToCollectionPlatform), nil - } - if isCollectionSet(input.Collection) { - return withCode(output, constants.ExitCodeBackToCollection), nil - } - return back(output), nil + output.Action = GameListActionBack + return output, nil } menuItems := make([]gaba.MenuItem, len(displayGames)) @@ -211,8 +208,8 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] } options := gaba.DefaultListOptions(title, menuItems) - options.SmallTitle = true - options.EnableImages = input.Config.ShowBoxArt + options.UseSmallTitle = true + options.ShowImages = input.Config.ShowBoxArt options.ActionButton = gabaconst.VirtualButtonX options.MultiSelectButton = gabaconst.VirtualButtonSelect options.DeselectAllButton = gabaconst.VirtualButtonL1 @@ -250,17 +247,13 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] output.SearchFilter = "" output.LastSelectedIndex = 0 output.LastSelectedPosition = 0 - return withCode(output, constants.ExitCodeClearSearch), nil - } - if isCollectionSet(input.Collection) && input.Platform.ID != 0 { - return withCode(output, constants.ExitCodeBackToCollectionPlatform), nil - } - if isCollectionSet(input.Collection) { - return withCode(output, constants.ExitCodeBackToCollection), nil + output.Action = GameListActionClearSearch + return output, nil } - return back(output), nil + output.Action = GameListActionBack + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } switch res.Action { @@ -272,22 +265,20 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput] output.LastSelectedIndex = res.Selected[0] output.LastSelectedPosition = res.VisiblePosition output.SelectedGames = selectedGames - return success(output), nil + output.Action = GameListActionSelected + return output, nil case gaba.ListActionTriggered: - return withCode(output, constants.ExitCodeSearch), nil + output.Action = GameListActionSearch + return output, nil case gaba.ListActionSecondaryTriggered: - return withCode(output, constants.ExitCodeBIOS), nil + output.Action = GameListActionBIOS + return output, nil } - if isCollectionSet(input.Collection) && input.Platform.ID != 0 { - return withCode(output, constants.ExitCodeBackToCollectionPlatform), nil - } - if isCollectionSet(input.Collection) { - return withCode(output, constants.ExitCodeBackToCollection), nil - } - return back(output), nil + output.Action = GameListActionBack + return output, nil } type loadGamesResult struct { diff --git a/ui/general_settings.go b/ui/general_settings.go index ee3ac2b..73b7eaf 100644 --- a/ui/general_settings.go +++ b/ui/general_settings.go @@ -14,6 +14,7 @@ type GeneralSettingsInput struct { } type GeneralSettingsOutput struct { + Action GeneralSettingsAction Config *internal.Config } @@ -23,9 +24,9 @@ func NewGeneralSettingsScreen() *GeneralSettingsScreen { return &GeneralSettingsScreen{} } -func (s *GeneralSettingsScreen) Draw(input GeneralSettingsInput) (ScreenResult[GeneralSettingsOutput], error) { +func (s *GeneralSettingsScreen) Draw(input GeneralSettingsInput) (GeneralSettingsOutput, error) { config := input.Config - output := GeneralSettingsOutput{Config: config} + output := GeneralSettingsOutput{Action: GeneralSettingsActionBack, Config: config} items := s.buildMenuItems(config) @@ -34,17 +35,17 @@ func (s *GeneralSettingsScreen) Draw(input GeneralSettingsInput) (ScreenResult[G gaba.OptionListSettings{ FooterHelpItems: OptionsListFooter(), StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("General settings error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } s.applySettings(config, result.Items) @@ -52,10 +53,11 @@ func (s *GeneralSettingsScreen) Draw(input GeneralSettingsInput) (ScreenResult[G err = internal.SaveConfig(config) if err != nil { gaba.GetLogger().Error("Error saving general settings", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = GeneralSettingsActionSaved + return output, nil } func (s *GeneralSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions { @@ -71,9 +73,9 @@ func (s *GeneralSettingsScreen) buildMenuItems(config *internal.Config) []gaba.I { Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_downloaded_games", Other: "Downloaded Games"}, nil)}, Options: []gaba.Option{ - {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_do_nothing", Other: "Do Nothing"}, nil), Value: "do_nothing"}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_mark", Other: "Mark"}, nil), Value: "mark"}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_filter", Other: "Filter"}, nil), Value: "filter"}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_do_nothing", Other: "Do Nothing"}, nil), Value: internal.DownloadedGamesModeDoNothing}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_mark", Other: "Mark"}, nil), Value: internal.DownloadedGamesModeMark}, + {DisplayName: i18n.Localize(&goi18n.Message{ID: "downloaded_games_filter", Other: "Filter"}, nil), Value: internal.DownloadedGamesModeFilter}, }, SelectedOption: downloadedGamesActionToIndex(config.DownloadedGames), }, @@ -121,7 +123,7 @@ func (s *GeneralSettingsScreen) applySettings(config *internal.Config, items []g } case i18n.Localize(&goi18n.Message{ID: "settings_downloaded_games", Other: "Downloaded Games"}, nil): - if val, ok := item.Options[item.SelectedOption].Value.(string); ok { + if val, ok := item.Options[item.SelectedOption].Value.(internal.DownloadedGamesMode); ok { config.DownloadedGames = val } @@ -143,13 +145,13 @@ func (s *GeneralSettingsScreen) applySettings(config *internal.Config, items []g } } -func downloadedGamesActionToIndex(action string) int { +func downloadedGamesActionToIndex(action internal.DownloadedGamesMode) int { switch action { - case "do_nothing": + case internal.DownloadedGamesModeDoNothing: return 0 - case "mark": + case internal.DownloadedGamesModeMark: return 1 - case "filter": + case internal.DownloadedGamesModeFilter: return 2 default: return 0 diff --git a/ui/info.go b/ui/info.go index 1f68fa4..2406302 100644 --- a/ui/info.go +++ b/ui/info.go @@ -2,7 +2,6 @@ package ui import ( "errors" - "grout/internal/constants" "grout/internal/imageutil" "grout/romm" "grout/version" @@ -18,6 +17,7 @@ type InfoInput struct { } type InfoOutput struct { + Action InfoAction LogoutRequested bool } @@ -27,8 +27,8 @@ func NewInfoScreen() *InfoScreen { return &InfoScreen{} } -func (s *InfoScreen) Draw(input InfoInput) (ScreenResult[InfoOutput], error) { - output := InfoOutput{} +func (s *InfoScreen) Draw(input InfoInput) (InfoOutput, error) { + output := InfoOutput{Action: InfoActionBack} sections := s.buildSections(input) @@ -37,7 +37,8 @@ func (s *InfoScreen) Draw(input InfoInput) (ScreenResult[InfoOutput], error) { options.ShowThemeBackground = false options.ShowScrollbar = true options.ActionButton = buttons.VirtualButtonX - options.EnableAction = true + options.AllowAction = true + options.ConfirmButton = buttons.VirtualButtonUnassigned result, err := gaba.DetailScreen("", options, []gaba.FooterHelpItem{ {ButtonName: "B", HelpText: i18n.Localize(&goi18n.Message{ID: "button_back", Other: "Back"}, nil)}, @@ -46,18 +47,19 @@ func (s *InfoScreen) Draw(input InfoInput) (ScreenResult[InfoOutput], error) { if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("Info screen error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } if result.Action == gaba.DetailActionTriggered { output.LogoutRequested = true - return withCode(output, constants.ExitCodeLogoutConfirm), nil + output.Action = InfoActionLogout + return output, nil } - return back(output), nil + return output, nil } func (s *InfoScreen) buildSections(input InfoInput) []gaba.Section { diff --git a/ui/login.go b/ui/login.go index 7bafe2f..905a8b7 100644 --- a/ui/login.go +++ b/ui/login.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "grout/internal" - "grout/internal/constants" "os" "strconv" "strings" @@ -24,8 +23,9 @@ type loginInput struct { } type loginOutput struct { - Host romm.Host - Config *internal.Config + Host romm.Host + Config *internal.Config + Cancelled bool } type loginAttemptResult struct { @@ -40,7 +40,7 @@ func newLoginScreen() *LoginScreen { return &LoginScreen{} } -func (s *LoginScreen) draw(input loginInput) (ScreenResult[loginOutput], error) { +func (s *LoginScreen) draw(input loginInput) (loginOutput, error) { host := input.ExistingHost // SSL option visibility - only show when HTTPS is selected @@ -180,7 +180,7 @@ func (s *LoginScreen) draw(input loginInput) (ScreenResult[loginOutput], error) ) if err != nil { - return withCode(loginOutput{}, gabagool.ExitCodeCancel), nil + return loginOutput{Cancelled: true}, nil } loginSettings := res.Items @@ -198,7 +198,7 @@ func (s *LoginScreen) draw(input loginInput) (ScreenResult[loginOutput], error) InsecureSkipVerify: loginSettings[5].Options[loginSettings[5].SelectedOption].Value.(bool), } - return success(loginOutput{Host: newHost}), nil + return loginOutput{Host: newHost}, nil } func LoginFlow(existingHost romm.Host) (*internal.Config, error) { @@ -214,11 +214,11 @@ func LoginFlow(existingHost romm.Host) (*internal.Config, error) { return nil, fmt.Errorf("unable to get login information: %w", err) } - if result.ExitCode == gabagool.ExitCodeBack || result.ExitCode == gabagool.ExitCodeCancel { + if result.Cancelled { os.Exit(1) } - host := result.Value.Host + host := result.Host loginResult := attemptLogin(host) @@ -243,7 +243,7 @@ func LoginFlow(existingHost romm.Host) (*internal.Config, error) { } func attemptLogin(host romm.Host) loginAttemptResult { - validationClient := romm.NewClientFromHost(host, constants.ValidationTimeout) + validationClient := romm.NewClientFromHost(host, internal.ValidationTimeout) result, _ := gabagool.ProcessMessage( i18n.Localize(&goi18n.Message{ID: "login_validating", Other: "Validating connection..."}, nil), @@ -254,7 +254,7 @@ func attemptLogin(host romm.Host) loginAttemptResult { return classifyLoginError(err), nil } - loginClient := romm.NewClientFromHost(host, constants.LoginTimeout) + loginClient := romm.NewClientFromHost(host, internal.LoginTimeout) err = loginClient.Login(host.Username, host.Password) if err != nil { return classifyLoginError(err), nil diff --git a/ui/logout_confirmation.go b/ui/logout_confirmation.go index afe36b8..dbd7c1e 100644 --- a/ui/logout_confirmation.go +++ b/ui/logout_confirmation.go @@ -2,7 +2,6 @@ package ui import ( "errors" - "grout/internal/constants" gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" buttons "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/constants" @@ -11,6 +10,7 @@ import ( ) type LogoutConfirmationOutput struct { + Action LogoutConfirmationAction Confirmed bool } @@ -20,28 +20,29 @@ func NewLogoutConfirmationScreen() *LogoutConfirmationScreen { return &LogoutConfirmationScreen{} } -func (s *LogoutConfirmationScreen) Draw() (ScreenResult[LogoutConfirmationOutput], error) { - output := LogoutConfirmationOutput{} +func (s *LogoutConfirmationScreen) Draw() (LogoutConfirmationOutput, error) { + output := LogoutConfirmationOutput{Action: LogoutConfirmationActionCancel} _, err := gaba.ConfirmationMessage( i18n.Localize(&goi18n.Message{ID: "logout_confirm_message", Other: "Are you sure you want to logout?"}, nil), []gaba.FooterHelpItem{ {ButtonName: "B", HelpText: i18n.Localize(&goi18n.Message{ID: "button_cancel", Other: "Cancel"}, nil)}, - {ButtonName: "Y", HelpText: i18n.Localize(&goi18n.Message{ID: "button_confirm", Other: "Confirm"}, nil)}, + {ButtonName: "X", HelpText: i18n.Localize(&goi18n.Message{ID: "button_confirm", Other: "Confirm"}, nil)}, }, gaba.MessageOptions{ - ConfirmButton: buttons.VirtualButtonY, + ConfirmButton: buttons.VirtualButtonX, }, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil // B button - cancel + return output, nil // B button - cancel } - return withCode(output, gaba.ExitCodeError), err + return output, err } - // A button pressed - confirm logout + // X button pressed - confirm logout output.Confirmed = true - return withCode(output, constants.ExitCodeLogout), nil + output.Action = LogoutConfirmationActionConfirm + return output, nil } diff --git a/ui/platform_selection.go b/ui/platform_selection.go index 6466749..0027a0a 100644 --- a/ui/platform_selection.go +++ b/ui/platform_selection.go @@ -3,7 +3,6 @@ package ui import ( "errors" "grout/internal" - "grout/internal/constants" "grout/romm" "sync/atomic" @@ -23,6 +22,7 @@ type PlatformSelectionInput struct { } type PlatformSelectionOutput struct { + Action PlatformSelectionAction SelectedPlatform romm.Platform LastSelectedIndex int LastSelectedPosition int @@ -35,14 +35,15 @@ func NewPlatformSelectionScreen() *PlatformSelectionScreen { return &PlatformSelectionScreen{} } -func (s *PlatformSelectionScreen) Draw(input PlatformSelectionInput) (ScreenResult[PlatformSelectionOutput], error) { +func (s *PlatformSelectionScreen) Draw(input PlatformSelectionInput) (PlatformSelectionOutput, error) { output := PlatformSelectionOutput{ + Action: PlatformSelectionActionQuit, LastSelectedIndex: input.LastSelectedIndex, LastSelectedPosition: input.LastSelectedPosition, } if len(input.Platforms) == 0 { - return withCode(output, gaba.ExitCode(404)), nil + return output, nil } var menuItems []gaba.MenuItem @@ -143,9 +144,10 @@ func (s *PlatformSelectionScreen) Draw(input PlatformSelectionInput) (ScreenResu if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + output.Action = PlatformSelectionActionQuit + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } switch sel.Action { @@ -157,21 +159,26 @@ func (s *PlatformSelectionScreen) Draw(input PlatformSelectionInput) (ScreenResu output.LastSelectedPosition = sel.VisiblePosition if platform.FSSlug == "collections" { - return withCode(output, constants.ExitCodeCollections), nil + output.Action = PlatformSelectionActionCollections + return output, nil } - return success(output), nil + output.Action = PlatformSelectionActionSelected + return output, nil case gaba.ListActionTriggered: if input.QuitOnBack { - return withCode(output, gaba.ExitCodeAction), nil + output.Action = PlatformSelectionActionSettings + return output, nil } case gaba.ListActionSecondaryTriggered: if input.QuitOnBack && input.ShowSaveSync != nil { - return withCode(output, constants.ExitCodeSaveSync), nil + output.Action = PlatformSelectionActionSaveSync + return output, nil } } - return withCode(output, gaba.ExitCodeBack), nil + output.Action = PlatformSelectionActionQuit + return output, nil } diff --git a/ui/rebuild_cache.go b/ui/rebuild_cache.go new file mode 100644 index 0000000..314fefe --- /dev/null +++ b/ui/rebuild_cache.go @@ -0,0 +1,85 @@ +package ui + +import ( + "grout/cache" + "grout/internal" + "grout/romm" + + gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" + "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" + goi18n "github.com/nicksnyder/go-i18n/v2/i18n" + uatomic "go.uber.org/atomic" +) + +type RebuildCacheInput struct { + Host romm.Host + Config *internal.Config + CacheSync *cache.BackgroundSync +} + +type RebuildCacheOutput struct { + Action RebuildCacheAction + UpdatedPlatforms []romm.Platform +} + +type RebuildCacheAction int + +const ( + RebuildCacheActionComplete RebuildCacheAction = iota + RebuildCacheActionError +) + +type RebuildCacheScreen struct{} + +func NewRebuildCacheScreen() *RebuildCacheScreen { + return &RebuildCacheScreen{} +} + +func (s *RebuildCacheScreen) Draw(input RebuildCacheInput) (RebuildCacheOutput, error) { + logger := gaba.GetLogger() + + if input.CacheSync != nil { + input.CacheSync.Stop() + } + + if err := cache.DeleteCacheFolder(); err != nil { + logger.Error("Failed to delete cache folder", "error", err) + } + + if err := cache.InitCacheManager(input.Host, input.Config); err != nil { + logger.Error("Failed to reinitialize cache manager", "error", err) + return RebuildCacheOutput{Action: RebuildCacheActionError}, err + } + + platforms, err := internal.GetMappedPlatforms(input.Host, input.Config.DirectoryMappings, input.Config.ApiTimeout) + if err != nil { + logger.Error("Failed to fetch platforms", "error", err) + return RebuildCacheOutput{Action: RebuildCacheActionError}, err + } + + platforms = internal.SortPlatformsByOrder(platforms, input.Config.PlatformOrder) + + cm := cache.GetCacheManager() + progress := uatomic.NewFloat64(0) + gaba.ProcessMessage( + i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil), + gaba.ProcessMessageOptions{ + ShowThemeBackground: true, + ShowProgressBar: true, + Progress: progress, + }, + func() (any, error) { + _, err := cm.PopulateFullCacheWithProgress(platforms, progress) + return nil, err + }, + ) + + if input.CacheSync != nil { + input.CacheSync.SetSynced() + } + + return RebuildCacheOutput{ + Action: RebuildCacheActionComplete, + UpdatedPlatforms: platforms, + }, nil +} diff --git a/ui/save_sync.go b/ui/save_sync.go index 89f1ac1..4e8aa59 100644 --- a/ui/save_sync.go +++ b/ui/save_sync.go @@ -31,7 +31,7 @@ func NewSaveSyncScreen() *SaveSyncScreen { return &SaveSyncScreen{} } -func (s *SaveSyncScreen) Draw(input SaveSyncInput) (ScreenResult[SaveSyncOutput], error) { +func (s *SaveSyncScreen) Draw(input SaveSyncInput) (SaveSyncOutput, error) { output := SaveSyncOutput{} config := input.Config @@ -145,7 +145,7 @@ func (s *SaveSyncScreen) Draw(input SaveSyncInput) (ScreenResult[SaveSyncOutput] }) } - return back(output), nil + return output, nil } func showFuzzyMatchConfirmation(fm sync.PendingFuzzyMatch) bool { diff --git a/ui/save_sync_settings.go b/ui/save_sync_settings.go index 69af2a0..60b8c46 100644 --- a/ui/save_sync_settings.go +++ b/ui/save_sync_settings.go @@ -19,6 +19,7 @@ type SaveSyncSettingsInput struct { } type SaveSyncSettingsOutput struct { + Action SaveSyncSettingsAction Config *internal.Config } @@ -30,15 +31,15 @@ func NewSaveSyncSettingsScreen() *SaveSyncSettingsScreen { return &SaveSyncSettingsScreen{} } -func (s *SaveSyncSettingsScreen) Draw(input SaveSyncSettingsInput) (ScreenResult[SaveSyncSettingsOutput], error) { +func (s *SaveSyncSettingsScreen) Draw(input SaveSyncSettingsInput) (SaveSyncSettingsOutput, error) { config := input.Config - output := SaveSyncSettingsOutput{Config: config} + output := SaveSyncSettingsOutput{Action: SaveSyncSettingsActionBack, Config: config} items := s.buildMenuItems(config) if len(items) == 0 { gaba.GetLogger().Warn("No platforms configured for save sync settings") - return back(output), nil + return output, nil } result, err := gaba.OptionsList( @@ -51,17 +52,17 @@ func (s *SaveSyncSettingsScreen) Draw(input SaveSyncSettingsInput) (ScreenResult }, InitialSelectedIndex: 0, StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } gaba.GetLogger().Error("Save sync settings error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } s.applySettings(config, result.Items) @@ -69,10 +70,11 @@ func (s *SaveSyncSettingsScreen) Draw(input SaveSyncSettingsInput) (ScreenResult err = internal.SaveConfig(config) if err != nil { gaba.GetLogger().Error("Error saving save sync settings", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = SaveSyncSettingsActionSaved + return output, nil } func (s *SaveSyncSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions { diff --git a/ui/screen_result.go b/ui/screen_result.go deleted file mode 100644 index 973fece..0000000 --- a/ui/screen_result.go +++ /dev/null @@ -1,29 +0,0 @@ -package ui - -import gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" - -type ScreenResult[T any] struct { - Value T - ExitCode gaba.ExitCode -} - -func success[T any](value T) ScreenResult[T] { - return ScreenResult[T]{ - Value: value, - ExitCode: gaba.ExitCodeSuccess, - } -} - -func back[T any](value T) ScreenResult[T] { - return ScreenResult[T]{ - Value: value, - ExitCode: gaba.ExitCodeBack, - } -} - -func withCode[T any](value T, code gaba.ExitCode) ScreenResult[T] { - return ScreenResult[T]{ - Value: value, - ExitCode: code, - } -} diff --git a/ui/search.go b/ui/search.go index 03a5003..f1b2e29 100644 --- a/ui/search.go +++ b/ui/search.go @@ -13,7 +13,8 @@ type SearchInput struct { } type SearchOutput struct { - Query string + Action SearchAction + Query string } type SearchScreen struct{} @@ -22,15 +23,15 @@ func NewSearchScreen() *SearchScreen { return &SearchScreen{} } -func (s *SearchScreen) Draw(input SearchInput) (ScreenResult[SearchOutput], error) { +func (s *SearchScreen) Draw(input SearchInput) (SearchOutput, error) { res, err := gaba.Keyboard(input.InitialText, i18n.Localize(&goi18n.Message{ID: "help_exit_text", Other: "Press any button to close help"}, nil)) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(SearchOutput{}), nil + return SearchOutput{Action: SearchActionCancel}, nil } gaba.GetLogger().Error("Error with keyboard", "error", err) - return withCode(SearchOutput{}, gaba.ExitCodeError), err + return SearchOutput{Action: SearchActionCancel}, err } - return success(SearchOutput{Query: res.Text}), nil + return SearchOutput{Action: SearchActionApply, Query: res.Text}, nil } diff --git a/ui/settings.go b/ui/settings.go index cacabe7..0d824fb 100644 --- a/ui/settings.go +++ b/ui/settings.go @@ -4,7 +4,6 @@ import ( "errors" "grout/cfw" "grout/internal" - "grout/internal/constants" "grout/romm" "sync/atomic" @@ -26,6 +25,7 @@ type SettingsInput struct { } type SettingsOutput struct { + Action SettingsAction Config *internal.Config GeneralSettingsClicked bool InfoClicked bool @@ -68,12 +68,12 @@ var settingsOrder = []SettingType{ SettingCheckUpdates, } -func (s *SettingsScreen) Draw(input SettingsInput) (ScreenResult[SettingsOutput], error) { +func (s *SettingsScreen) Draw(input SettingsInput) (SettingsOutput, error) { config := input.Config - output := SettingsOutput{Config: config} + output := SettingsOutput{Action: SettingsActionBack, Config: config} visibility := &settingsVisibility{} - visibility.saveSyncSettings.Store(config.SaveSyncMode != "off") + visibility.saveSyncSettings.Store(config.SaveSyncMode != internal.SaveSyncModeOff) items := s.buildMenuItems(config, visibility) @@ -84,16 +84,16 @@ func (s *SettingsScreen) Draw(input SettingsInput) (ScreenResult[SettingsOutput] InitialSelectedIndex: input.LastSelectedIndex, VisibleStartIndex: input.LastVisibleStartIndex, StatusBar: StatusBar(), - SmallTitle: true, + UseSmallTitle: true, }, items, ) if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(SettingsOutput{}), nil + return SettingsOutput{Action: SettingsActionBack}, nil } - return withCode(SettingsOutput{}, gaba.ExitCodeError), err + return SettingsOutput{Action: SettingsActionBack}, err } output.LastSelectedIndex = result.Selected @@ -107,42 +107,50 @@ func (s *SettingsScreen) Draw(input SettingsInput) (ScreenResult[SettingsOutput] if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_general", Other: "General"}, nil) { output.GeneralSettingsClicked = true - return withCode(output, constants.ExitCodeGeneralSettings), nil + output.Action = SettingsActionGeneral + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_info", Other: "Grout Info"}, nil) { output.InfoClicked = true - return withCode(output, constants.ExitCodeInfo), nil + output.Action = SettingsActionInfo + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_collections", Other: "Collections Settings"}, nil) { output.CollectionsSettingsClicked = true - return withCode(output, constants.ExitCodeCollectionsSettings), nil + output.Action = SettingsActionCollections + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_edit_mappings", Other: "Directory Mappings"}, nil) { output.DirectoryMappingsClicked = true - return withCode(output, constants.ExitCodeEditMappings), nil + output.Action = SettingsActionPlatformMapping + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_advanced", Other: "Advanced"}, nil) { output.AdvancedSettingsClicked = true - return withCode(output, constants.ExitCodeAdvancedSettings), nil + output.Action = SettingsActionAdvanced + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_save_sync_settings", Other: "Save Sync Settings"}, nil) { output.SaveSyncSettingsClicked = true - return withCode(output, constants.ExitCodeSaveSyncSettings), nil + output.Action = SettingsActionSaveSync + return output, nil } if selectedText == i18n.Localize(&goi18n.Message{ID: "update_check_for_updates", Other: "Check for Updates"}, nil) { output.CheckUpdatesClicked = true - return withCode(output, constants.ExitCodeCheckUpdate), nil + output.Action = SettingsActionCheckUpdate + return output, nil } } output.Config = config - return success(output), nil + output.Action = SettingsActionSaved + return output, nil } func (s *SettingsScreen) buildMenuItems(config *internal.Config, visibility *settingsVisibility) []gaba.ItemWithOptions { @@ -177,13 +185,13 @@ func (s *SettingsScreen) buildMenuItem(settingType SettingType, config *internal return gaba.ItemWithOptions{ Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_save_sync", Other: "Save Sync"}, nil)}, Options: []gaba.Option{ - {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_off", Other: "Off"}, nil), Value: "off", OnUpdate: func(v interface{}) { + {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_off", Other: "Off"}, nil), Value: internal.SaveSyncModeOff, OnUpdate: func(v interface{}) { visibility.saveSyncSettings.Store(false) }}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_manual", Other: "Manual"}, nil), Value: "manual", OnUpdate: func(v interface{}) { + {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_manual", Other: "Manual"}, nil), Value: internal.SaveSyncModeManual, OnUpdate: func(v interface{}) { visibility.saveSyncSettings.Store(true) }}, - {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_automatic", Other: "Automatic"}, nil), Value: "automatic", OnUpdate: func(v interface{}) { + {DisplayName: i18n.Localize(&goi18n.Message{ID: "save_sync_mode_automatic", Other: "Automatic"}, nil), Value: internal.SaveSyncModeAutomatic, OnUpdate: func(v interface{}) { visibility.saveSyncSettings.Store(true) }}, }, @@ -229,7 +237,7 @@ func (s *SettingsScreen) applySettings(config *internal.Config, items []gaba.Ite text := item.Item.Text switch text { case i18n.Localize(&goi18n.Message{ID: "settings_save_sync", Other: "Save Sync"}, nil): - if val, ok := item.Options[item.SelectedOption].Value.(string); ok { + if val, ok := item.Options[item.SelectedOption].Value.(internal.SaveSyncMode); ok { config.SaveSyncMode = val } } @@ -243,16 +251,16 @@ func boolToIndex(b bool) int { return 0 } -func logLevelToIndex(level string) int { +func logLevelToIndex(level internal.LogLevel) int { switch level { - case "DEBUG": + case internal.LogLevelDebug: return 0 - case "INFO": + case internal.LogLevelInfo: return 1 - case "ERROR": + case internal.LogLevelError: return 2 default: - return 1 // Default to INFO + return 1 } } @@ -292,24 +300,24 @@ func languageToIndex(lang string) int { } } -func saveSyncModeToIndex(mode string) int { +func saveSyncModeToIndex(mode internal.SaveSyncMode) int { switch mode { - case "off": + case internal.SaveSyncModeOff: return 0 - case "manual": + case internal.SaveSyncModeManual: return 1 - case "automatic": + case internal.SaveSyncModeAutomatic: return 2 default: return 0 } } -func collectionViewToIndex(view string) int { +func collectionViewToIndex(view internal.CollectionView) int { switch view { - case "platform": + case internal.CollectionViewPlatform: return 0 - case "unified": + case internal.CollectionViewUnified: return 1 default: return 0 diff --git a/ui/settings_platform_mapping.go b/ui/settings_platform_mapping.go index 7dff50b..7e10bed 100644 --- a/ui/settings_platform_mapping.go +++ b/ui/settings_platform_mapping.go @@ -32,6 +32,7 @@ type PlatformMappingInput struct { } type PlatformMappingOutput struct { + Action PlatformMappingAction Mappings map[string]internal.DirectoryMapping } @@ -41,20 +42,20 @@ func NewPlatformMappingScreen() *PlatformMappingScreen { return &PlatformMappingScreen{} } -func (s *PlatformMappingScreen) Draw(input PlatformMappingInput) (ScreenResult[PlatformMappingOutput], error) { +func (s *PlatformMappingScreen) Draw(input PlatformMappingInput) (PlatformMappingOutput, error) { logger := gaba.GetLogger() - output := PlatformMappingOutput{Mappings: make(map[string]internal.DirectoryMapping)} + output := PlatformMappingOutput{Action: PlatformMappingActionBack, Mappings: make(map[string]internal.DirectoryMapping)} rommPlatforms, err := s.fetchPlatforms(input) if err != nil { logger.Error("Error fetching RomM Platforms", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } romDirectories, err := s.getRomDirectories(input.RomDirectory) if err != nil { logger.Error("Error fetching ROM directories", "error", err) - return withCode(output, gaba.ExitCodeBack), err + return output, err } mappingOptions := s.buildMappingOptions(rommPlatforms, romDirectories, input) @@ -79,19 +80,20 @@ func (s *PlatformMappingScreen) Draw(input PlatformMappingInput) (ScreenResult[P if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(PlatformMappingOutput{}), nil + return PlatformMappingOutput{Action: PlatformMappingActionBack}, nil } - return withCode(PlatformMappingOutput{}, gaba.ExitCodeError), err + return output, err } output.Mappings = s.buildMappingsFromResult(result.Items) if err := s.createDirectories(output.Mappings, input.RomDirectory, romDirectories); err != nil { logger.Error("Error creating directories", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } - return success(output), nil + output.Action = PlatformMappingActionSaved + return output, nil } func (s *PlatformMappingScreen) fetchPlatforms(input PlatformMappingInput) ([]romm.Platform, error) { diff --git a/ui/sync_report.go b/ui/sync_report.go index 8942a89..eb09ece 100644 --- a/ui/sync_report.go +++ b/ui/sync_report.go @@ -25,7 +25,7 @@ func newSyncReportScreen() *SyncReportScreen { return &SyncReportScreen{} } -func (s *SyncReportScreen) draw(input syncReportInput) (ScreenResult[syncReportOutput], error) { +func (s *SyncReportScreen) draw(input syncReportInput) (syncReportOutput, error) { logger := gaba.GetLogger() output := syncReportOutput{} @@ -42,17 +42,17 @@ func (s *SyncReportScreen) draw(input syncReportInput) (ScreenResult[syncReportO if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } logger.Error("Detail screen error", "error", err) - return withCode(output, gaba.ExitCodeError), err + return output, err } if result.Action == gaba.DetailActionCancelled { - return back(output), nil + return output, nil } - return success(output), nil + return output, nil } func (s *SyncReportScreen) buildSections(results []sync.SyncResult, unmatched []sync.UnmatchedSave) []gaba.Section { diff --git a/ui/update.go b/ui/update.go index 84b10c3..b46d6e8 100644 --- a/ui/update.go +++ b/ui/update.go @@ -23,6 +23,7 @@ type UpdateInput struct { } type UpdateOutput struct { + Action UpdateCheckAction UpdatePerformed bool } @@ -32,9 +33,9 @@ func NewUpdateScreen() *UpdateScreen { return &UpdateScreen{} } -func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], error) { +func (s *UpdateScreen) Draw(input UpdateInput) (UpdateOutput, error) { logger := gaba.GetLogger() - output := UpdateOutput{} + output := UpdateOutput{Action: UpdateCheckActionComplete} var updateInfo *update.Info var checkErr error @@ -64,7 +65,7 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro }, gaba.MessageOptions{}, ) - return back(output), nil + return output, nil } if !updateInfo.UpdateAvailable { @@ -75,7 +76,7 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro }, gaba.MessageOptions{}, ) - return back(output), nil + return output, nil } updateMessage := fmt.Sprintf( @@ -98,9 +99,9 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro if err != nil { if errors.Is(err, gaba.ErrCancelled) { - return back(output), nil + return output, nil } - return withCode(output, gaba.ExitCodeError), err + return output, err } progress := &atomic.Float64{} @@ -133,7 +134,7 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro }, gaba.MessageOptions{}, ) - return back(output), nil + return output, nil } gaba.ConfirmationMessage( @@ -145,5 +146,5 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro ) output.UpdatePerformed = true - return withCode(output, gaba.ExitCodeSuccess), nil + return output, nil } diff --git a/update/updater.go b/update/updater.go index d4df37d..6cda442 100644 --- a/update/updater.go +++ b/update/updater.go @@ -4,7 +4,6 @@ import ( "fmt" "grout/cfw" "grout/internal" - "grout/internal/constants" "grout/romm" "grout/version" "io" @@ -149,7 +148,7 @@ func PerformUpdate(downloadURL string, progress *atomic.Float64) error { func downloadBinary(url, destPath string, progress *atomic.Float64) error { client := &http.Client{ - Timeout: constants.UpdaterTimeout, + Timeout: internal.UpdaterTimeout, } req, err := http.NewRequest(http.MethodGet, url, nil) From 85e2f92164523db3b9edf8bee89cc9a3cf8bcc00 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Wed, 21 Jan 2026 00:10:11 -0500 Subject: [PATCH 4/6] Cleanup unused code after refactor --- bios/bios.go | 5 ---- bios/data/core_subdirectories.json | 38 ------------------------------ cache/artwork.go | 10 -------- cache/errors.go | 6 ++--- cache/games.go | 8 ------- cache/manager.go | 12 +++++----- internal/imageutil/imageutil.go | 6 ++--- sync/auto_sync.go | 4 ++-- sync/roms.go | 4 ++-- sync/save_sync.go | 18 +++++++------- ui/actions.go | 21 ----------------- ui/download.go | 14 +++++------ ui/emulator_selection.go | 4 ---- ui/footer_helpers.go | 18 -------------- ui/games_list.go | 6 ++--- ui/save_sync.go | 4 ++-- ui/sync_report.go | 4 ++-- update/version.go | 2 +- 18 files changed, 39 insertions(+), 145 deletions(-) delete mode 100644 bios/data/core_subdirectories.json diff --git a/bios/bios.go b/bios/bios.go index 9abc8f3..283c40e 100644 --- a/bios/bios.go +++ b/bios/bios.go @@ -23,11 +23,6 @@ func mustLoadJSONMap[K comparable, V any](path string) map[K]V { var LibretroCoreToBIOS = mustLoadJSONMap[string, CoreBIOS]("data/core_requirements.json") var PlatformToLibretroCores = mustLoadJSONMap[string, []string]("data/platform_cores.json") -// CoreBIOSSubdirectories maps Libretro core names (without _libretro suffix) -// to their required BIOS subdirectory within the system BIOS directory. -// Cores not in this map use the root BIOS directory. -var CoreBIOSSubdirectories = mustLoadJSONMap[string, string]("data/core_subdirectories.json") - // File represents a single BIOS/firmware file requirement type File struct { FileName string // e.g., "gba_bios.bin" diff --git a/bios/data/core_subdirectories.json b/bios/data/core_subdirectories.json deleted file mode 100644 index 56d6ae5..0000000 --- a/bios/data/core_subdirectories.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "bk": "bk", - "bluemsx": "", - "dolphin": "dolphin-emu/Sys", - "ep128emu_core": "ep128emu/roms", - "fbneo": "fbneo", - "fbneo_cps12": "fbneo", - "fbneo_neogeo": "fbneo", - "flycast": "dc", - "flycast_gles2": "dc", - "fuse": "fuse", - "galaksija": "galaksija", - "higan_sfc": "", - "higan_sfc_balanced": "", - "kronos": "kronos", - "mkxp-z": "mkxp-z/RTP", - "mupen64plus_next": "Mupen64plus", - "mupen64plus_next_develop": "Mupen64plus", - "mupen64plus_next_gles2": "Mupen64plus", - "mupen64plus_next_gles3": "Mupen64plus", - "neocd": "neocd", - "np2kai": "np2kai", - "pico": "pico8", - "pico-8": "pico8", - "pcsx2": "pcsx2", - "ppsspp": "PPSSPP", - "px68k": "keropi", - "quasi88": "quasi88", - "retrodream": "dc", - "same_cdi": "same_cdi/bios", - "scummvm": "scummvm", - "vice_x128": "vice", - "vice_x64": "vice", - "vice_x64dtv": "vice", - "vice_x64sc": "vice", - "vice_xscpu64": "vice", - "x1": "xmil" -} diff --git a/cache/artwork.go b/cache/artwork.go index 101fb4b..4897c3f 100644 --- a/cache/artwork.go +++ b/cache/artwork.go @@ -99,16 +99,6 @@ func isValidPNG(path string) bool { return err == nil } -func GetRomsWithArtwork(roms []romm.Rom) []romm.Rom { - var withArtwork []romm.Rom - for _, rom := range roms { - if HasArtworkURL(rom) { - withArtwork = append(withArtwork, rom) - } - } - return withArtwork -} - func GetMissingArtwork(roms []romm.Rom) []romm.Rom { var missing []romm.Rom for _, rom := range roms { diff --git a/cache/errors.go b/cache/errors.go index af752b5..a600d07 100644 --- a/cache/errors.go +++ b/cache/errors.go @@ -6,10 +6,8 @@ import ( ) var ( - ErrNotInitialized = errors.New("cache manager not initialized") - ErrCacheMiss = errors.New("cache miss") - ErrDBClosed = errors.New("database connection closed") - ErrInvalidCacheKey = errors.New("invalid cache key") + ErrNotInitialized = errors.New("cache manager not initialized") + ErrCacheMiss = errors.New("cache miss") ) type Error struct { diff --git a/cache/games.go b/cache/games.go index adffae3..8ac0111 100644 --- a/cache/games.go +++ b/cache/games.go @@ -772,14 +772,6 @@ func RecordFailedLookup(fsSlug, localFilename string) error { return cm.RecordFailedLookup(fsSlug, localFilename) } -func ShouldAttemptLookup(fsSlug, localFilename string) bool { - cm := GetCacheManager() - if cm == nil { - return true - } - return cm.ShouldAttemptLookup(fsSlug, localFilename) -} - func ShouldAttemptLookupWithNextRetry(fsSlug, localFilename string) (bool, time.Time) { cm := GetCacheManager() if cm == nil { diff --git a/cache/manager.go b/cache/manager.go index 1fc443d..7c7ab46 100644 --- a/cache/manager.go +++ b/cache/manager.go @@ -22,10 +22,10 @@ type Manager struct { config Config initialized bool - stats *CacheStats + stats *Stats } -type CacheStats struct { +type Stats struct { mu sync.Mutex Hits int64 Misses int64 @@ -33,21 +33,21 @@ type CacheStats struct { LastAccess time.Time } -func (s *CacheStats) recordHit() { +func (s *Stats) recordHit() { s.mu.Lock() s.Hits++ s.LastAccess = time.Now() s.mu.Unlock() } -func (s *CacheStats) recordMiss() { +func (s *Stats) recordMiss() { s.mu.Lock() s.Misses++ s.LastAccess = time.Now() s.mu.Unlock() } -func (s *CacheStats) recordError() { +func (s *Stats) recordError() { s.mu.Lock() s.Errors++ s.mu.Unlock() @@ -101,7 +101,7 @@ func newCacheManager(host romm.Host, config Config) (*Manager, error) { host: host, config: config, initialized: true, - stats: &CacheStats{}, + stats: &Stats{}, } logger.Debug("Cache manager initialized", "path", dbPath) diff --git a/internal/imageutil/imageutil.go b/internal/imageutil/imageutil.go index 866b60e..b9c4e23 100644 --- a/internal/imageutil/imageutil.go +++ b/internal/imageutil/imageutil.go @@ -9,13 +9,13 @@ import ( "os" "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" - go_qr "github.com/piglig/go-qr" + goqr "github.com/piglig/go-qr" "golang.org/x/image/draw" _ "golang.org/x/image/webp" // Register WebP decoder ) func CreateTempQRCode(content string, size int) (string, error) { - qr, err := go_qr.EncodeText(content, go_qr.Low) + qr, err := goqr.EncodeText(content, goqr.Low) if err != nil { return "", err } @@ -26,7 +26,7 @@ func CreateTempQRCode(content string, size int) (string, error) { } tempFile.Close() - config := go_qr.NewQrCodeImgConfig(size/10, 0) + config := goqr.NewQrCodeImgConfig(size/10, 0) if err := qr.PNG(config, tempFile.Name()); err != nil { return "", err } diff --git a/sync/auto_sync.go b/sync/auto_sync.go index 3da4848..c99eefb 100644 --- a/sync/auto_sync.go +++ b/sync/auto_sync.go @@ -95,10 +95,10 @@ func (a *AutoSync) run() { logger.Debug("AutoSync: No syncs needed") } return - } else { - logger.Debug("AutoSync: Found syncs", "count", len(syncs)) } + logger.Debug("AutoSync: Found syncs", "count", len(syncs)) + hadError := false for i := range syncs { diff --git a/sync/roms.go b/sync/roms.go index 17f4833..485fb84 100644 --- a/sync/roms.go +++ b/sync/roms.go @@ -43,7 +43,7 @@ func (lrf LocalRomFile) baseName() string { return strings.TrimSuffix(lrf.FileName, filepath.Ext(lrf.FileName)) } -func (lrf LocalRomFile) syncAction() SyncAction { +func (lrf LocalRomFile) syncAction() Action { hasLocal := lrf.SaveFile != nil baseName := lrf.baseName() @@ -54,7 +54,7 @@ func (lrf LocalRomFile) syncAction() SyncAction { return Skip case hasLocal && !hasRemote: return Upload - case !hasLocal && hasRemote: + case !hasLocal: return Download } diff --git a/sync/save_sync.go b/sync/save_sync.go index 483cdc7..ffcc435 100644 --- a/sync/save_sync.go +++ b/sync/save_sync.go @@ -27,21 +27,21 @@ type SaveSync struct { GameBase string Local *LocalSave Remote romm.Save - Action SyncAction + Action Action } -type SyncAction string +type Action string const ( - Download SyncAction = "DOWNLOAD" - Upload SyncAction = "UPLOAD" - Skip SyncAction = "SKIP" + Download Action = "DOWNLOAD" + Upload Action = "UPLOAD" + Skip Action = "SKIP" ) -type SyncResult struct { +type Result struct { GameName string RomDisplayName string - Action SyncAction + Action Action Success bool Error string Err error @@ -84,7 +84,7 @@ type MatchAttemptResult struct { MatchesAttempted []string } -func (s *SaveSync) Execute(host romm.Host, config *internal.Config) SyncResult { +func (s *SaveSync) Execute(host romm.Host, config *internal.Config) Result { logger := gaba.GetLogger() displayName := s.RomName @@ -92,7 +92,7 @@ func (s *SaveSync) Execute(host romm.Host, config *internal.Config) SyncResult { displayName = strings.TrimSuffix(displayName, filepath.Ext(displayName)) } - result := SyncResult{ + result := Result{ GameName: s.GameBase, RomDisplayName: displayName, Action: s.Action, diff --git a/ui/actions.go b/ui/actions.go index c6b97e8..24ba974 100644 --- a/ui/actions.go +++ b/ui/actions.go @@ -126,29 +126,8 @@ const ( LogoutConfirmationActionCancel ) -type SaveSyncAction int - -const ( - SaveSyncActionComplete SaveSyncAction = iota - SaveSyncActionBack -) - -type BIOSDownloadAction int - -const ( - BIOSDownloadActionComplete BIOSDownloadAction = iota - BIOSDownloadActionBack -) - -type ArtworkSyncAction int - -const ( - ArtworkSyncActionComplete ArtworkSyncAction = iota -) - type UpdateCheckAction int const ( UpdateCheckActionComplete UpdateCheckAction = iota - UpdateCheckActionBack ) diff --git a/ui/download.go b/ui/download.go index 7dbb7de..9b5444a 100644 --- a/ui/download.go +++ b/ui/download.go @@ -27,7 +27,7 @@ import ( "go.uber.org/atomic" ) -type downloadInput struct { +type DownloadInput struct { Config internal.Config Host romm.Host Platform romm.Platform @@ -37,7 +37,7 @@ type downloadInput struct { SelectedFileID int } -type downloadOutput struct { +type DownloadOutput struct { DownloadedGames []romm.Rom Platform romm.Platform AllGames []romm.Rom @@ -56,8 +56,8 @@ func NewDownloadScreen() *DownloadScreen { return &DownloadScreen{} } -func (s *DownloadScreen) Execute(config internal.Config, host romm.Host, platform romm.Platform, selectedGames []romm.Rom, allGames []romm.Rom, searchFilter string, selectedFileID int) downloadOutput { - result, err := s.draw(downloadInput{ +func (s *DownloadScreen) Execute(config internal.Config, host romm.Host, platform romm.Platform, selectedGames []romm.Rom, allGames []romm.Rom, searchFilter string, selectedFileID int) DownloadOutput { + result, err := s.draw(DownloadInput{ Config: config, Host: host, Platform: platform, @@ -69,7 +69,7 @@ func (s *DownloadScreen) Execute(config internal.Config, host romm.Host, platfor if err != nil { gaba.GetLogger().Error("Download failed", "error", err) - return downloadOutput{ + return DownloadOutput{ AllGames: allGames, Platform: platform, SearchFilter: searchFilter, @@ -83,10 +83,10 @@ func (s *DownloadScreen) Execute(config internal.Config, host romm.Host, platfor return result } -func (s *DownloadScreen) draw(input downloadInput) (downloadOutput, error) { +func (s *DownloadScreen) draw(input DownloadInput) (DownloadOutput, error) { logger := gaba.GetLogger() - output := downloadOutput{ + output := DownloadOutput{ Platform: input.Platform, AllGames: input.AllGames, SearchFilter: input.SearchFilter, diff --git a/ui/emulator_selection.go b/ui/emulator_selection.go index acaf298..553500d 100644 --- a/ui/emulator_selection.go +++ b/ui/emulator_selection.go @@ -33,10 +33,6 @@ type EmulatorSelectionOutput struct { type EmulatorSelectionScreen struct{} -func NewEmulatorSelectionScreen() *EmulatorSelectionScreen { - return &EmulatorSelectionScreen{} -} - func (s *EmulatorSelectionScreen) Draw(input EmulatorSelectionInput) (EmulatorSelectionOutput, error) { output := EmulatorSelectionOutput{ LastSelectedIndex: input.LastSelectedIndex, diff --git a/ui/footer_helpers.go b/ui/footer_helpers.go index 87d469b..1da9bb5 100644 --- a/ui/footer_helpers.go +++ b/ui/footer_helpers.go @@ -15,24 +15,10 @@ func footerItem(button, msgID, fallback string) gaba.FooterHelpItem { } func FooterContinue() gaba.FooterHelpItem { return footerItem("A", "button_continue", "Continue") } -func FooterSelect() gaba.FooterHelpItem { return footerItem("A", "button_select", "Select") } -func FooterConfirm() gaba.FooterHelpItem { return footerItem("A", "button_confirm", "Confirm") } func FooterDownload() gaba.FooterHelpItem { return footerItem("A", "button_download", "Download") } func FooterBack() gaba.FooterHelpItem { return footerItem("B", "button_back", "Back") } func FooterCancel() gaba.FooterHelpItem { return footerItem("B", "button_cancel", "Cancel") } -func FooterClose() gaba.FooterHelpItem { return footerItem("B", "button_close", "Close") } func FooterQuit() gaba.FooterHelpItem { return footerItem("B", "button_quit", "Quit") } -func FooterSearch() gaba.FooterHelpItem { return footerItem("X", "button_search", "Search") } -func FooterSettings() gaba.FooterHelpItem { return footerItem("X", "button_settings", "Settings") } -func FooterOptions() gaba.FooterHelpItem { return footerItem("X", "button_options", "Options") } -func FooterLogout() gaba.FooterHelpItem { return footerItem("X", "button_logout", "Logout") } -func FooterBIOS() gaba.FooterHelpItem { return footerItem("Y", "button_bios", "BIOS") } -func FooterSaveSync() gaba.FooterHelpItem { return footerItem("Y", "button_save_sync", "Sync") } -func FooterMenu() gaba.FooterHelpItem { return footerItem("Start", "button_menu", "Menu") } - -func FooterStartConfirm() gaba.FooterHelpItem { - return footerItem("Start", "button_confirm", "Confirm") -} func FooterSave() gaba.FooterHelpItem { return footerItem(icons.Start, "button_save", "Save") @@ -49,7 +35,3 @@ func ContinueFooter() []gaba.FooterHelpItem { func OptionsListFooter() []gaba.FooterHelpItem { return []gaba.FooterHelpItem{FooterCancel(), FooterCycle(), FooterSave()} } - -func BackSelectFooter() []gaba.FooterHelpItem { - return []gaba.FooterHelpItem{FooterBack(), FooterSelect()} -} diff --git a/ui/games_list.go b/ui/games_list.go index 1308eaf..8610522 100644 --- a/ui/games_list.go +++ b/ui/games_list.go @@ -314,7 +314,7 @@ func (s *GameListScreen) loadGames(input GameListInput) (loadGamesResult, error) if ft == ftPlatform { cached, err = cm.GetPlatformGames(id) - } else if ft == ftCollection { + } else { cached, err = cm.GetCollectionGames(collection) } @@ -417,7 +417,7 @@ func (s *GameListScreen) loadGames(input GameListInput) (loadGamesResult, error) wg.Add(1) go func() { defer wg.Done() - roms, err := fetchList(config, host, id, ft) + roms, err := fetchList(id, ft) if err != nil { logger.Error("Error downloading game list", "error", err) gamesFetchErr = err @@ -528,7 +528,7 @@ func (s *GameListScreen) showErrorMessage(err error) { ) } -func fetchList(config *internal.Config, host romm.Host, queryID int, fetchType fetchType) ([]romm.Rom, error) { +func fetchList(queryID int, fetchType fetchType) ([]romm.Rom, error) { logger := gaba.GetLogger() cm := cache.GetCacheManager() diff --git a/ui/save_sync.go b/ui/save_sync.go index 4e8aa59..9fb22ec 100644 --- a/ui/save_sync.go +++ b/ui/save_sync.go @@ -61,7 +61,7 @@ func (s *SaveSyncScreen) Draw(input SaveSyncInput) (SaveSyncOutput, error) { return scanResult{Syncs: syncs, Unmatched: unmatched, FuzzyMatches: fuzzyMatches}, nil }) - var results []sync.SyncResult + var results []sync.Result var unmatched []sync.UnmatchedSave if scan, ok := scanData.(scanResult); ok { @@ -101,7 +101,7 @@ func (s *SaveSyncScreen) Draw(input SaveSyncInput) (SaveSyncOutput, error) { } } - results = make([]sync.SyncResult, 0, len(syncs)) + results = make([]sync.Result, 0, len(syncs)) if len(syncs) > 0 { progress := &atomic.Float64{} diff --git a/ui/sync_report.go b/ui/sync_report.go index eb09ece..f742e72 100644 --- a/ui/sync_report.go +++ b/ui/sync_report.go @@ -13,7 +13,7 @@ import ( ) type syncReportInput struct { - Results []sync.SyncResult + Results []sync.Result Unmatched []sync.UnmatchedSave } @@ -55,7 +55,7 @@ func (s *SyncReportScreen) draw(input syncReportInput) (syncReportOutput, error) return output, nil } -func (s *SyncReportScreen) buildSections(results []sync.SyncResult, unmatched []sync.UnmatchedSave) []gaba.Section { +func (s *SyncReportScreen) buildSections(results []sync.Result, unmatched []sync.UnmatchedSave) []gaba.Section { logger := gaba.GetLogger() logger.Debug("Building sync report", "totalResults", len(results), "unmatched", len(unmatched)) diff --git a/update/version.go b/update/version.go index f367dd1..4bef14f 100644 --- a/update/version.go +++ b/update/version.go @@ -119,7 +119,7 @@ func CompareVersions(current, latest string) int { // Current is prerelease, latest is full release - latest is newer return -1 } - if currentHasPrerelease && latestHasPrerelease { + if currentHasPrerelease { // Both are prereleases - compare prerelease strings lexicographically // For simplicity, we'll just do a string comparison // In practice, this handles cases like "beta.1" vs "beta.2" From 508bce7bf11a6587f60d208a7d96c105e2388b74 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Wed, 21 Jan 2026 00:12:11 -0500 Subject: [PATCH 5/6] Better sql error comparison --- cache/games.go | 7 ++++--- cache/platforms.go | 3 ++- internal/config.go | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cache/games.go b/cache/games.go index 8ac0111..30db68d 100644 --- a/cache/games.go +++ b/cache/games.go @@ -3,6 +3,7 @@ package cache import ( "database/sql" "encoding/json" + "errors" "fmt" "grout/cfw" "grout/internal/stringutil" @@ -279,7 +280,7 @@ func (cm *Manager) getCollectionInternalID(collection romm.Collection) (int64, e err = cm.db.QueryRow(`SELECT id FROM collections WHERE romm_id = ? AND type = ?`, collection.ID, collType).Scan(&id) } - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { cm.stats.recordMiss() return 0, ErrCacheMiss } @@ -559,7 +560,7 @@ func (cm *Manager) GetRomIDByFilename(fsSlug, filename string) (int, string, boo return romID, romName, true } - if err != sql.ErrNoRows { + if !errors.Is(err, sql.ErrNoRows) { cm.stats.recordError() gaba.GetLogger().Debug("ROM lookup error", "fsSlug", slug, "filename", filename, "error", err) } @@ -576,7 +577,7 @@ func (cm *Manager) GetRomIDByFilename(fsSlug, filename string) (int, string, boo return romID, romName, true } - if err != sql.ErrNoRows { + if !errors.Is(err, sql.ErrNoRows) { cm.stats.recordError() gaba.GetLogger().Debug("Filename mapping lookup error", "fsSlug", slug, "filename", filename, "error", err) } diff --git a/cache/platforms.go b/cache/platforms.go index 25c4df2..5f63291 100644 --- a/cache/platforms.go +++ b/cache/platforms.go @@ -3,6 +3,7 @@ package cache import ( "database/sql" "encoding/json" + "errors" "grout/romm" "time" @@ -133,7 +134,7 @@ func (cm *Manager) HasBIOS(platformID int) (bool, bool) { SELECT has_bios FROM bios_availability WHERE platform_id = ? `, platformID).Scan(&hasBIOS) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return false, false } if err != nil { diff --git a/internal/config.go b/internal/config.go index 51c5848..6d9c6fc 100644 --- a/internal/config.go +++ b/internal/config.go @@ -184,7 +184,6 @@ func (c *Config) LoadPlatformsBinding(host romm.Host, timeout ...time.Duration) return nil } -// To decouple a circular dependency func (c Config) GetApiTimeout() time.Duration { return c.ApiTimeout } func (c Config) GetShowCollections() bool { return c.ShowRegularCollections } func (c Config) GetShowSmartCollections() bool { return c.ShowSmartCollections } From 18375ce2648d1b3ca0457fe5c3a1ccb8c68de2d0 Mon Sep 17 00:00:00 2001 From: "Brandon T. Kowalski" Date: Wed, 21 Jan 2026 00:13:34 -0500 Subject: [PATCH 6/6] Bump gaba version --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b28d8a1..e74a5b8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.2 require ( github.com/BrandonKowalski/certifiable v1.3.0 - github.com/BrandonKowalski/gabagool/v2 v2.7.0 + github.com/BrandonKowalski/gabagool/v2 v2.7.1 github.com/beevik/etree v1.6.0 github.com/bodgit/sevenzip v1.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1 diff --git a/go.sum b/go.sum index 25e75e0..af0c8ce 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/BrandonKowalski/gabagool/v2 v2.6.4 h1:yMvcDFgNnIBpuKnLuAllZpNX0dMYijy github.com/BrandonKowalski/gabagool/v2 v2.6.4/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= github.com/BrandonKowalski/gabagool/v2 v2.7.0 h1:M6WfMTCDtdTa6B3vPlhTB+3GlE9bpBd3gfCWIa0ncJ4= github.com/BrandonKowalski/gabagool/v2 v2.7.0/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= +github.com/BrandonKowalski/gabagool/v2 v2.7.1 h1:dKzPkC/UF0GVMxftOHl9x25FxuKp9oXxrQ2WGaRrmsQ= +github.com/BrandonKowalski/gabagool/v2 v2.7.1/go.mod h1:Iq/YFTz7+w8O78IE21kOkHbg3a4ZD2mjcd0S/jVhI94= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=