Skip to content

Commit f19364b

Browse files
Merge pull request #179 from rommapp/v4.8.0.0
V4.8.0.0
2 parents ea0cf61 + 60f055e commit f19364b

143 files changed

Lines changed: 5229 additions & 1487 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 326 additions & 45 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Build
99
build64/
1010
build32/
11+
dist/
1112
build-miyoo/
1213
grout
1314
app/app
@@ -17,6 +18,7 @@ es_restart_request
1718
# Local config
1819
.env
1920
config.json
21+
!scripts/**/config.json
2022
config-*.json
2123

2224
# Cache & temp

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
<br />
77
<img src=".github/resources/logotipo.png" height="45px" width="auto" alt="romm grout logotype">
88
<h3 style="font-size: 25px;">
9-
A RomM Client for <a href="https://muos.dev">muOS</a>, <a href="https://knulli.org">Knulli</a>, <a href="https://rocknix.org">ROCKNIX</a>, <a href="https://spruceui.github.io/">Spruce (v4)</a>, <a href="https://nextui.loveretro.games">NextUI</a> and <a href="https://trimui.com">TrimUI</a>.
9+
A RomM Client for your retro doo-dad.
1010
</h3>
1111

12+
[Allium][allium] · [Batocera][batocera] · [Knulli][knulli] · [muOS][muos] · [NextUI][nextui] · [Onion][onion] · [ROCKNIX][rocknix] · [Spruce][spruce] · [TrimUI][trimui]
13+
1214
<br>
1315

1416

@@ -92,6 +94,18 @@ If you enjoy using Grout and feel inclined to pay it forward, go do something ni
9294

9395
✌🏻
9496

97+
<!-- CFW Links -->
98+
99+
[allium]: https://github.com/goweiwen/Allium
100+
[batocera]: https://batocera.org
101+
[knulli]: https://knulli.org
102+
[muos]: https://muos.dev
103+
[nextui]: https://nextui.loveretro.games
104+
[onion]: https://onionui.github.io
105+
[rocknix]: https://rocknix.org
106+
[spruce]: https://spruceui.github.io/
107+
[trimui]: https://github.com/trimui
108+
95109
<!-- Badges -->
96110

97111
[docs-badge-img]: https://img.shields.io/badge/user%20guide-007C77?style=for-the-badge

app/grout.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"grout/cfw"
5+
"grout/update"
56
"os"
67

78
_ "github.com/BrandonKowalski/certifiable"
@@ -31,5 +32,6 @@ func cleanup() {
3132
if err := os.RemoveAll(".tmp"); err != nil {
3233
gaba.GetLogger().Error("Failed to clean .tmp directory", "error", err)
3334
}
35+
update.CleanupUpdateArtifacts()
3436
gaba.Close()
3537
}

app/helpers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func handlePlatformMappingUpdateUI(state *AppState, r ui.PlatformMappingOutput)
5959
state.Config.PlatformOrder = internal.PrunePlatformOrder(state.Config.PlatformOrder, r.Mappings)
6060
internal.SaveConfig(state.Config)
6161

62-
platforms, err := internal.GetMappedPlatforms(state.Host, r.Mappings, state.Config.ApiTimeout)
62+
platforms, err := internal.GetMappedPlatforms(state.Host, r.Mappings, state.Config.ApiTimeout.Duration())
6363
if err != nil {
6464
gaba.GetLogger().Error("Failed to refresh platforms after mapping update", "error", err)
6565
return
@@ -120,7 +120,7 @@ func handleLogout(state *AppState) {
120120
screen := ui.NewPlatformMappingScreen()
121121
result, err := screen.Draw(ui.PlatformMappingInput{
122122
Host: state.Config.Hosts[0],
123-
ApiTimeout: state.Config.ApiTimeout,
123+
ApiTimeout: state.Config.ApiTimeout.Duration(),
124124
CFW: state.CFW,
125125
RomDirectory: cfw.GetRomDirectory(),
126126
AutoSelect: false,
@@ -135,10 +135,10 @@ func handleLogout(state *AppState) {
135135
}
136136

137137
if err := cache.InitCacheManager(state.Config.Hosts[0], state.Config); err != nil {
138-
logger.Error("Failed to initialize cache manager after re-login", "error", err)
138+
logger.Error("Failed to re-initialize cache manager", "error", err)
139139
}
140140

141-
platforms, err := internal.GetMappedPlatforms(state.Config.Hosts[0], state.Config.DirectoryMappings, state.Config.ApiTimeout)
141+
platforms, err := internal.GetMappedPlatforms(state.Config.Hosts[0], state.Config.DirectoryMappings, state.Config.ApiTimeout.Duration())
142142
if err != nil {
143143
logger.Error("Failed to load platforms after re-login", "error", err)
144144
return

app/router.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func registerScreens(r *router.Router, state *AppState) {
177177
r.Register(ScreenArtworkSync, func(input any) (any, error) {
178178
in := input.(ui.ArtworkSyncInput)
179179
screen := ui.NewArtworkSyncScreen()
180-
screen.Execute(in.Config, in.Host)
180+
screen.Execute(in)
181181
return ui.ArtworkSyncOutput{}, nil
182182
})
183183

@@ -228,6 +228,20 @@ func registerScreens(r *router.Router, state *AppState) {
228228
return screen.Draw(input.(ui.SaveMappingInput))
229229
})
230230

231+
r.Register(ScreenSwitchToToken, func(input any) (any, error) {
232+
screen := ui.NewSwitchToTokenScreen()
233+
result := screen.Execute(state.Config, state.Host)
234+
if result.Success {
235+
state.Host = result.Host
236+
}
237+
return result, nil
238+
})
239+
240+
r.Register(ScreenToolsSettings, func(input any) (any, error) {
241+
screen := ui.NewToolsSettingsScreen()
242+
return screen.Draw(input.(ui.ToolsSettingsInput))
243+
})
244+
231245
r.Register(ScreenServerAddress, func(input any) (any, error) {
232246
screen := ui.NewServerAddressScreen()
233247
return screen.Draw(input.(ui.ServerAddressInput))

app/screens.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ const (
3636
ScreenSyncHistory
3737
ScreenSaveMapping
3838
ScreenServerAddress
39+
ScreenToolsSettings
40+
ScreenSwitchToToken
3941
)

app/setup.go

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"grout/cfw"
77
"grout/cfw/allium"
88
"grout/cfw/muos"
9+
"grout/cfw/onion"
910
"grout/internal"
1011
"grout/internal/environment"
1112
"grout/internal/fileutil"
@@ -46,6 +47,8 @@ func setup() SetupResult {
4647
mappingBytes, mappingErr = muos.GetInputMappingBytes()
4748
case cfw.Allium:
4849
mappingBytes, mappingErr = allium.GetInputMappingBytes()
50+
case cfw.Onion:
51+
mappingBytes, mappingErr = onion.GetInputMappingBytes()
4952
}
5053
if mappingBytes != nil && mappingErr == nil {
5154
gaba.SetInputMappingBytes(mappingBytes)
@@ -117,8 +120,8 @@ func setup() SetupResult {
117120
if config == nil {
118121
config = &internal.Config{
119122
ShowRegularCollections: true,
120-
ApiTimeout: 30 * time.Minute,
121-
DownloadTimeout: 60 * time.Minute,
123+
ApiTimeout: internal.DurationSeconds(30 * time.Second),
124+
DownloadTimeout: internal.DurationSeconds(60 * time.Minute),
122125
}
123126
}
124127
config.Language = selectedLanguage
@@ -130,6 +133,7 @@ func setup() SetupResult {
130133
loginConfig, loginErr := ui.LoginFlow(romm.Host{})
131134
if loginErr != nil {
132135
logger.Error("Login flow failed", "error", loginErr)
136+
gaba.Close()
133137
log.SetOutput(os.Stderr)
134138
log.Fatalf("Login failed: %v", loginErr)
135139
}
@@ -176,7 +180,7 @@ func setup() SetupResult {
176180
screen := ui.NewPlatformMappingScreen()
177181
result, err := screen.Draw(ui.PlatformMappingInput{
178182
Host: config.Hosts[0],
179-
ApiTimeout: config.ApiTimeout,
183+
ApiTimeout: config.ApiTimeout.Duration(),
180184
CFW: currentCFW,
181185
RomDirectory: cfw.GetRomDirectory(),
182186
AutoSelect: false,
@@ -198,32 +202,111 @@ func setup() SetupResult {
198202
}
199203

200204
var platforms []romm.Platform
201-
var loadErr error
202205

203206
splashBytes, _ := resources.GetSplashImageBytes()
204207

205208
for {
209+
var connErr error
210+
var authErr error
211+
var loadErr error
212+
206213
gaba.ProcessMessage("", gaba.ProcessMessageOptions{
207214
ImageBytes: splashBytes,
208215
ImageWidth: 768,
209216
ImageHeight: 540,
210217
}, func() (interface{}, error) {
211-
// Load platform bindings from RomM server (non-fatal if it fails)
212-
if err := config.LoadPlatformsBinding(config.Hosts[0], config.ApiTimeout); err != nil {
218+
host := config.Hosts[0]
219+
220+
// Step 1: Validate server connectivity
221+
client := romm.NewClient(host.URL(), romm.WithInsecureSkipVerify(host.InsecureSkipVerify), romm.WithTimeout(internal.ValidationTimeout))
222+
if err := client.ValidateConnection(); err != nil {
223+
connErr = err
224+
return nil, nil
225+
}
226+
227+
// Step 2: Validate credentials/token
228+
authClient := romm.NewClientFromHost(host, internal.LoginTimeout)
229+
if host.HasTokenAuth() {
230+
if err := authClient.ValidateToken(); err != nil {
231+
authErr = err
232+
return nil, nil
233+
}
234+
// Re-fetch username if missing
235+
if host.Username == "" {
236+
if user, err := authClient.GetCurrentUser(); err == nil {
237+
host.Username = user.Username
238+
config.Hosts[0] = host
239+
internal.SaveConfig(config)
240+
}
241+
}
242+
} else {
243+
if err := authClient.Login(host.Username, host.Password); err != nil {
244+
authErr = err
245+
return nil, nil
246+
}
247+
}
248+
249+
// Step 3: Load platforms
250+
if err := config.LoadPlatformsBinding(config.Hosts[0], config.ApiTimeout.Duration()); err != nil {
213251
logger.Debug("Failed to load platform bindings", "error", err)
214252
}
215253

216254
var err error
217-
platforms, err = internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout)
255+
platforms, err = internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout.Duration())
218256
if err != nil {
219257
loadErr = err
220-
return nil, err
258+
return nil, nil
221259
}
222-
loadErr = nil
223260
platforms = internal.SortPlatformsByOrder(platforms, config.PlatformOrder)
224261
return nil, nil
225262
})
226263

264+
// Handle connectivity failure
265+
if connErr != nil {
266+
logger.Warn("Server connectivity failed", "error", connErr)
267+
errorMessage := classifyStartupError(connErr)
268+
errorMsg := i18n.Localize(errorMessage, nil)
269+
retry := showStartupError(errorMsg)
270+
if !retry {
271+
gaba.Close()
272+
os.Exit(1)
273+
}
274+
continue
275+
}
276+
277+
// Handle auth failure — show message and offer re-login
278+
if authErr != nil {
279+
logger.Warn("Auth validation failed", "error", authErr)
280+
281+
var msg string
282+
if config.Hosts[0].HasTokenAuth() {
283+
msg = i18n.Localize(&goi18n.Message{ID: "startup_error_token_invalid", Other: "Your API token is invalid or expired.\nPlease set up a new one."}, nil)
284+
} else {
285+
msg = i18n.Localize(&goi18n.Message{ID: "startup_error_credentials_invalid", Other: "Your credentials are invalid.\nPlease log in again."}, nil)
286+
}
287+
288+
gaba.ConfirmationMessage(msg, []gaba.FooterHelpItem{
289+
{ButtonName: "A", HelpText: i18n.Localize(&goi18n.Message{ID: "button_continue", Other: "Continue"}, nil)},
290+
}, gaba.MessageOptions{})
291+
292+
loginConfig, loginErr := ui.LoginFlow(config.Hosts[0])
293+
if loginErr != nil {
294+
logger.Error("Re-login failed", "error", loginErr)
295+
gaba.Close()
296+
log.SetOutput(os.Stderr)
297+
log.Fatalf("Login failed: %v", loginErr)
298+
}
299+
config.Hosts = loginConfig.Hosts
300+
config.PlatformsBinding = loginConfig.PlatformsBinding
301+
internal.SaveConfig(config)
302+
303+
// Re-initialize cache manager with new credentials
304+
if err := cache.InitCacheManager(config.Hosts[0], config); err != nil {
305+
logger.Error("Failed to re-initialize cache manager", "error", err)
306+
}
307+
continue
308+
}
309+
227310
if loadErr == nil {
228311
break
229312
}

app/transitions.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ func buildTransitionFunc(state *AppState, quitOnBack bool, initialShowCollection
5656
return transitionGeneralSettings(ctx, result)
5757
case ScreenCollectionsSettings:
5858
return transitionCollectionsSettings(ctx, result)
59+
case ScreenToolsSettings:
60+
return transitionToolsSettings(ctx, result)
5961
case ScreenAdvancedSettings:
6062
return transitionAdvancedSettings(ctx, result)
6163
case ScreenPlatformMapping:
@@ -70,6 +72,14 @@ func buildTransitionFunc(state *AppState, quitOnBack bool, initialShowCollection
7072
return popOrExit(stack)
7173
case ScreenArtworkSync:
7274
return popOrExit(stack)
75+
case ScreenSwitchToToken:
76+
// Pop the stacked settings entry so we don't go back to stale state
77+
stack.Pop()
78+
return ScreenSettings, ui.SettingsInput{
79+
Config: ctx.state.Config,
80+
CFW: ctx.state.CFW,
81+
Host: ctx.state.Host,
82+
}
7383
case ScreenUpdateCheck:
7484
return transitionUpdateCheck(ctx, result)
7585
case ScreenGameFilters:
@@ -616,6 +626,14 @@ func transitionSettings(ctx *transitionContext, result any) (router.Screen, any)
616626
ctx.stack.Push(ScreenSettings, pushInput, r)
617627
return ScreenCollectionsSettings, ui.CollectionsSettingsInput{Config: ctx.state.Config}
618628

629+
case ui.SettingsActionSwitchToToken:
630+
ctx.stack.Push(ScreenSettings, pushInput, r)
631+
return ScreenSwitchToToken, nil
632+
633+
case ui.SettingsActionTools:
634+
ctx.stack.Push(ScreenSettings, pushInput, r)
635+
return ScreenToolsSettings, ui.ToolsSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}
636+
619637
case ui.SettingsActionAdvanced:
620638
ctx.stack.Push(ScreenSettings, pushInput, r)
621639
return ScreenAdvancedSettings, ui.AdvancedSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}
@@ -624,7 +642,7 @@ func transitionSettings(ctx *transitionContext, result any) (router.Screen, any)
624642
ctx.stack.Push(ScreenSettings, pushInput, r)
625643
return ScreenPlatformMapping, ui.PlatformMappingInput{
626644
Host: ctx.state.Host,
627-
ApiTimeout: ctx.state.Config.ApiTimeout,
645+
ApiTimeout: ctx.state.Config.ApiTimeout.Duration(),
628646
CFW: ctx.state.CFW,
629647
RomDirectory: cfw.GetRomDirectory(),
630648
ExistingMappings: ctx.state.Config.DirectoryMappings,
@@ -689,6 +707,25 @@ func transitionCollectionsSettings(ctx *transitionContext, result any) (router.S
689707
return popOrExit(ctx.stack)
690708
}
691709

710+
func transitionToolsSettings(ctx *transitionContext, result any) (router.Screen, any) {
711+
r := result.(ui.ToolsSettingsOutput)
712+
713+
pushInput := ui.ToolsSettingsInput{Config: ctx.state.Config, Host: ctx.state.Host}
714+
715+
switch r.Action {
716+
case ui.ToolsSettingsActionSyncLocalArtwork:
717+
ctx.stack.Push(ScreenToolsSettings, pushInput, r)
718+
return ScreenArtworkSync, ui.ArtworkSyncInput{
719+
Config: *ctx.state.Config,
720+
Host: ctx.state.Host,
721+
DownloadedOnly: true,
722+
}
723+
724+
default:
725+
return popOrExit(ctx.stack)
726+
}
727+
}
728+
692729
func transitionAdvancedSettings(ctx *transitionContext, result any) (router.Screen, any) {
693730
r := result.(ui.AdvancedSettingsOutput)
694731

cache/artwork.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func DownloadAndCacheArtwork(rom romm.Rom, kind artutil.ArtKind, host romm.Host)
138138
if err != nil {
139139
return fmt.Errorf("failed to create request: %w", err)
140140
}
141-
req.Header.Set("Authorization", host.BasicAuthHeader())
141+
req.Header.Set("Authorization", host.AuthHeader())
142142

143143
client := &http.Client{Timeout: romm.DefaultClientTimeout}
144144
resp, err := client.Do(req)

0 commit comments

Comments
 (0)