Skip to content

commit 481ba0f

abduznik edited this page May 23, 2026 · 1 revision

fix(beta): UI polish, steam deck paste, linux presets, github workflow, 500 error

Commit: 481ba0f1bb0314dfc9f7fb856ad97da58b0377a0

Author: abduznik

Date: 2026-04-28

Why: Fixes a bug or regression in the existing codebase.

Files Changed

.github/workflows/release.yml         |   6 +-
 lib/core/romm/romm_service.dart       |  10 +++-
 lib/providers/download_provider.dart  |   2 -
 lib/ui/screens/onboarding_screen.dart | 101 +++++++++++++++++++++++++---------
 lib/ui/screens/settings_screen.dart   |  92 +++++++++++++++++++++++++------
 5 files changed, 165 insertions(+), 46 deletions(-)
  • .github/workflows/release.yml
  • lib/core/romm/romm_service.dart
  • lib/providers/download_provider.dart
  • lib/ui/screens/onboarding_screen.dart
  • lib/ui/screens/settings_screen.dart

Diff

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3034b81..0a27c18 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,10 @@ on:
         description: 'Version tag (e.g. v1.0.0)'
         required: true
         default: 'v1.0.0'
+      prerelease:
+        description: 'Is Pre-release?'
+        type: boolean
+        default: false
       build_linux:
         description: 'Build Linux?'
         type: boolean
@@ -186,6 +190,6 @@ jobs:
             artifacts/freegosy-macos.dmg
           fail_on_unmatched_files: false
           draft: false
-          prerelease: false
+          prerelease: ${{ github.event.inputs.prerelease == 'true' }}
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index e031153..6b9bcdb 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -350,10 +350,16 @@ class RommService {
     }
     final baseUrl = _normalizeBaseUrl(_config.baseUrl);
     final name = game.fileName ?? game.fsName ?? game.name;
-    String encoded = Uri.encodeComponent(name);
+    String encoded = Uri.encodeComponent(name)
+        .replaceAll("'", "%27")
+        .replaceAll("(", "%28")
+        .replaceAll(")", "%29");
     if (encoded.length > 100) {
       final ext = p.extension(name); final stem = p.basenameWithoutExtension(name);
-      encoded = Uri.encodeComponent('${stem.substring(0, min(stem.length, 50))}$ext');
+      encoded = Uri.encodeComponent('${stem.substring(0, min(stem.length, 50))}$ext')
+          .replaceAll("'", "%27")
+          .replaceAll("(", "%28")
+          .replaceAll(")", "%29");
     }
     return '$baseUrl/api/roms/${game.id}/content/$encoded';
   }
diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart
index aac0528..9e01f0c 100644
--- a/lib/providers/download_provider.dart
+++ b/lib/providers/download_provider.dart
@@ -22,7 +22,6 @@ final downloadServiceProvider = FutureProvider<DownloadService?>((ref) async {
     connectTimeout: const Duration(seconds: 60),
     receiveTimeout: const Duration(hours: 4), // Allow up to 4 hours for large ROMs
     headers: {
-      'Accept-Encoding': 'identity', // Disable compression for large downloads to avoid proxy issues
       'User-Agent': 'Freegosy/${AppConstants.version}',
     },
   ));
@@ -53,7 +52,6 @@ final emulatorDownloadServiceProvider =
     connectTimeout: const Duration(seconds: 60),
     receiveTimeout: const Duration(minutes: 30),
     headers: {
-      'Accept-Encoding': 'identity',
       'User-Agent': 'Freegosy/${AppConstants.version}',
     },
   ));
diff --git a/lib/ui/screens/onboarding_screen.dart b/lib/ui/screens/onboarding_screen.dart
index f7e8c63..37282e8 100644
--- a/lib/ui/screens/onboarding_screen.dart
+++ b/lib/ui/screens/onboarding_screen.dart
@@ -1,5 +1,6 @@
 import 'dart:io' as io;
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:path/path.dart' as p;
@@ -32,6 +33,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
   // Step 2: Storage Config
   String? _romsRoot;
   String? _emusRoot;
+  String? _presetRoot;
   String _linuxPreset = 'default';
   bool _isStorageInitialized = false;
 
@@ -49,6 +51,12 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
     final romsRoot = prefs.getString('romsRootPath');
     final emusRoot = prefs.getString('emulatorsRootPath');
     final linuxPreset = prefs.getString('linuxSyncPreset') ?? 'default';
+    String? presetRoot;
+    if (linuxPreset == 'emudeck') {
+      presetRoot = prefs.getString('emudeckRootPath');
+    } else if (linuxPreset == 'retrodeck') {
+      presetRoot = prefs.getString('retrodeckRootPath');
+    }
 
     if (!mounted) return;
 
@@ -60,6 +68,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
       if (romsRoot != null) _romsRoot = romsRoot;
       if (emusRoot != null) _emusRoot = emusRoot;
       _linuxPreset = linuxPreset;
+      if (presetRoot != null) _presetRoot = presetRoot;
       if (romsRoot != null || emusRoot != null) {
         _isStorageInitialized = true;
       }
@@ -128,14 +137,25 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
     await prefs.setString('rommBaseUrl', _baseUrlController.text.trim());
     await SecureStorageService.write('rommApiKey', _apiKeyController.text.trim(), prefs);
     
+    if (io.Platform.isLinux && _linuxPreset != 'default' && _presetRoot != null) {
+      if (_linuxPreset == 'emudeck') {
+        _romsRoot = p.join(_presetRoot!, 'roms');
+        _emusRoot = p.join(_presetRoot!, 'tools', 'launchers');
+        await prefs.setString('emudeckRootPath', _presetRoot!);
+      } else if (_linuxPreset == 'retrodeck') {
+        _romsRoot = p.join(_presetRoot!, 'roms');
+        _emusRoot = p.join(_presetRoot!, 'tools');
+        await prefs.setString('retrodeckRootPath', _presetRoot!);
+      }
+      await prefs.setString('linuxSyncPreset', _linuxPreset);
+    } else if (io.Platform.isLinux) {
+      await prefs.setString('linuxSyncPreset', 'default');
+    }
+    
     // Save Storage Config
     if (_romsRoot != null) await prefs.setString('romsRootPath', _romsRoot!);
     if (_emusRoot != null) await prefs.setString('emulatorsRootPath', _emusRoot!);
     
-    if (io.Platform.isLinux) {
-      await prefs.setString('linuxSyncPreset', _linuxPreset);
-    }
-
     // Invalidate providers to trigger reload
     ref.invalidate(rommConfigProvider);
     ref.invalidate(rommServiceProvider);
@@ -261,19 +281,39 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
           const SizedBox(height: 40),
           TextField(
             controller: _baseUrlController,
-            decoration: const InputDecoration(
+            decoration: InputDecoration(
               labelText: 'Server URL',
               hintText: 'http://your-ip:8080',
-              prefixIcon: Icon(Icons.dns),
+              prefixIcon: const Icon(Icons.dns),
+              suffixIcon: IconButton(
+                icon: const Icon(Icons.paste),
+                onPressed: () async {
+                  final data = await Clipboard.getData(Clipboard.kTextPlain);
+                  if (data != null && data.text != null) {
+                    _baseUrlController.text = data.text!;
+                  }
+                },
+                tooltip: 'Paste from clipboard',
+              ),
             ),
           ),
           const SizedBox(height: 20),
           TextField(
             controller: _apiKeyController,
-            decoration: const InputDecoration(
+            decoration: InputDecoration(
               labelText: 'API Key',
               hintText: 'Found in RomM User Settings',
-              prefixIcon: Icon(Icons.key),
+              prefixIcon: const Icon(Icons.key),
+              suffixIcon: IconButton(
+                icon: const Icon(Icons.paste),
+                onPressed: () async {
+                  final data = await Clipboard.getData(Clipboard.kTextPlain);
+                  if (data != null && data.text != null) {
+                    _apiKeyController.text = data.text!;
+                  }
+                },
+                tooltip: 'Paste from clipboard',
+              ),
             ),
             obscureText: true,
           ),
@@ -367,23 +407,34 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
             const SizedBox(height: 32),
           ],
 
-          _buildPathSelector(
-            label: 'ROMs Directory',
-            currentPath: _romsRoot ?? 'Loading...',
-            onTap: () async {
-              final path = await FilePicker.platform.getDirectoryPath();
-              if (path != null) setState(() => _romsRoot = path);
-            },
-          ),
-          const SizedBox(height: 24),
-          _buildPathSelector(
-            label: 'Emulators Directory',
-            currentPath: _emusRoot ?? 'Loading...',
-            onTap: () async {
-              final path = await FilePicker.platform.getDirectoryPath();
-              if (path != null) setState(() => _emusRoot = path);
-            },
-          ),
+          if (io.Platform.isLinux && _linuxPreset != 'default') ...[
+            _buildPathSelector(
+              label: '${_linuxPreset == 'emudeck' ? 'EmuDeck' : 'RetroDeck'} Installation Root',
+              currentPath: _presetRoot ?? 'Select root directory...',
+              onTap: () async {
+                final path = await FilePicker.platform.getDirectoryPath();
+                if (path != null) setState(() => _presetRoot = path);
+              },
+            ),
+          ] else ...[
+            _buildPathSelector(
+              label: 'ROMs Directory',
+              currentPath: _romsRoot ?? 'Loading...',
+              onTap: () async {
+                final path = await FilePicker.platform.getDirectoryPath();
+                if (path != null) setState(() => _romsRoot = path);
+              },
+            ),
+            const SizedBox(height: 24),
+            _buildPathSelector(
+              label: 'Emulators Directory',
+              currentPath: _emusRoot ?? 'Loading...',
+              onTap: () async {
+                final path = await FilePicker.platform.getDirectoryPath();
+                if (path != null) setState(() => _emusRoot = path);
+              },
+            ),
+          ],
         ],
       ),
     );
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index 7619b64..5938745 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -1,3 +1,4 @@
+import 'dart:io' as io;
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -236,22 +237,81 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
 
   // --- Storage Section ---
   Widget _buildStorageSection(DirectoryService directoryService) {
+    final prefs = ref.read(sharedPreferencesProvider);
+    final preset = directoryService.linuxSyncPreset;
+    
     return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
       const Text('Storage', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
       const SizedBox(height: 16),
-      _buildPathRow(
-        label: 'ROMs Directory',
-        currentPath: directoryService.romsRootPath,
-        onChanged: (p) async { if (p != null) { await directoryService.setRomsRoot(p); ref.invalidate(directoryServiceProvider); } },
-        onReset: () async { await directoryService.resetRomsRoot(); ref.invalidate(directoryServiceProvider); },
-      ),
-      const SizedBox(height: 12),
-      _buildPathRow(
-        label: 'Emulators Directory',
-        currentPath: directoryService.emulatorsRootPath,
-        onChanged: (p) async { if (p != null) { await directoryService.setEmulatorsRoot(p); ref.invalidate(directoryServiceProvider); } },
-        onReset: () async { await directoryService.resetEmulatorsRoot(); ref.invalidate(directoryServiceProvider); },
-      ),
+      
+      if (io.Platform.isLinux) ...[
+        const Text('Linux App Layout', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
+        const SizedBox(height: 8),
+        DropdownButtonFormField<String>(
+          initialValue: preset,
+          decoration: const InputDecoration(border: OutlineInputBorder()),
+          items: const [
+            DropdownMenuItem(value: 'default', child: Text('Manual / Native')),
+            DropdownMenuItem(value: 'emudeck', child: Text('EmuDeck')),
+            DropdownMenuItem(value: 'retrodeck', child: Text('RetroDeck')),
+          ],
+          onChanged: (val) async {
+            if (val != null) {
+              await directoryService.setLinuxSyncPreset(val);
+              ref.invalidate(directoryServiceProvider);
+            }
+          },
+        ),
+        const SizedBox(height: 16),
+      ],
+
+      if (io.Platform.isLinux && preset != 'default') ...[
+        _buildPathRow(
+          label: '${preset == 'emudeck' ? 'EmuDeck' : 'RetroDeck'} Installation Root',
+          currentPath: preset == 'emudeck' 
+              ? (directoryService.emudeckRootPath ?? 'Not set') 
+              : (prefs.getString('retrodeckRootPath') ?? 'Not set'),
+          onChanged: (p) async { 
+            if (p != null) { 
+              if (preset == 'emudeck') {
+                await directoryService.setEmudeckRoot(p);
+              } else {
+                await prefs.setString('retrodeckRootPath', p);
+                // Trigger re-init so paths compute
+                await directoryService.initialize();
+              }
+              ref.invalidate(directoryServiceProvider); 
+            } 
+          },
+        ),
+        const SizedBox(height: 12),
+        // Read-only views of the computed paths
+        _buildPathRow(
+          label: 'Computed ROMs Directory',
+          currentPath: directoryService.romsRootPath,
+          onChanged: null,
+        ),
+        const SizedBox(height: 12),
+        _buildPathRow(
+          label: 'Computed Emulators Directory',
+          currentPath: directoryService.emulatorsRootPath,
+          onChanged: null,
+        ),
+      ] else ...[
+        _buildPathRow(
+          label: 'ROMs Directory',
+          currentPath: directoryService.romsRootPath,
+          onChanged: (p) async { if (p != null) { await directoryService.setRomsRoot(p); ref.invalidate(directoryServiceProvider); } },
+          onReset: () async { await directoryService.resetRomsRoot(); ref.invalidate(directoryServiceProvider); },
+        ),
+        const SizedBox(height: 12),
+        _buildPathRow(
+          label: 'Emulators Directory',
+          currentPath: directoryService.emulatorsRootPath,
+          onChanged: (p) async { if (p != null) { await directoryService.setEmulatorsRoot(p); ref.invalidate(directoryServiceProvider); } },
+          onReset: () async { await directoryService.resetEmulatorsRoot(); ref.invalidate(directoryServiceProvider); },
+        ),
+      ],
       const SizedBox(height: 16),
       OutlinedButton.icon(
         onPressed: () => SystemUtils.openDirectory(directoryService.romsRootPath),
@@ -267,13 +327,13 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
     ]);
   }
 
-  Widget _buildPathRow({required String label, required String currentPath, required Function(String?) onChanged, VoidCallback? onReset}) {
+  Widget _buildPathRow({required String label, required String currentPath, required Function(String?)? onChanged, VoidCallback? onReset}) {
     return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
       Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
       Row(children: [
         Expanded(child: Text(currentPath, overflow: TextOverflow.ellipsis)),
-        IconButton(onPressed: onReset, icon: const Icon(Icons.restore)),
-        ElevatedButton(onPressed: () async => onChanged(await FilePicker.platform.getDirectoryPath()), child: const Text('Change')),
+        if (onReset != null) IconButton(onPressed: onReset, icon: const Icon(Icons.restore)),
+        if (onChanged != null) ElevatedButton(onPressed: () async => onChanged(await FilePicker.platform.getDirectoryPath()), child: const Text('Change')),
       ]),
     ]);
   }

Clone this wiki locally