Skip to content

commit 85987ed

abduznik edited this page May 23, 2026 · 1 revision

feat: unlock linux path flexibility and fix retrodeck SD card root resolution

Commit: 85987ed39e6455886dde20955462842f2b612aa5

Author: abduznik

Date: 2026-05-12

Why: Adds a new feature or capability to the application.

Files Changed

jobs.json                                          |  1 +
 .../linux_strategies/emudeck_strategy.dart         |  2 +
 .../linux_strategies/retrodeck_strategy.dart       |  9 ++-
 lib/core/storage/directory_service.dart            | 48 ++++++-------
 lib/ui/screens/library_actions.dart                |  5 +-
 lib/ui/screens/settings_screen.dart                | 84 ++++++++--------------
 test/core/emulator/linux_strategies_test.dart      | 30 ++++++++
 test/unit/firmware_service_test.mocks.dart         |  8 +--
 test/unit/save_sync_service_test.mocks.dart        |  8 +--
 test/unit/strategy_registry_test.mocks.dart        |  8 +--
 test/widgets/library_screen_test.mocks.dart        |  8 +--
 test/widgets/settings_screen_test.mocks.dart       |  8 +--
 12 files changed, 117 insertions(+), 102 deletions(-)
  • jobs.json
  • lib/core/emulator/linux_strategies/emudeck_strategy.dart
  • lib/core/emulator/linux_strategies/retrodeck_strategy.dart
  • lib/core/storage/directory_service.dart
  • lib/ui/screens/library_actions.dart
  • lib/ui/screens/settings_screen.dart
  • test/core/emulator/linux_strategies_test.dart
  • test/unit/firmware_service_test.mocks.dart
  • test/unit/save_sync_service_test.mocks.dart
  • test/unit/strategy_registry_test.mocks.dart
  • test/widgets/library_screen_test.mocks.dart
  • test/widgets/settings_screen_test.mocks.dart

Diff

diff --git a/jobs.json b/jobs.json
new file mode 100644
index 0000000..ed60395
--- /dev/null
+++ b/jobs.json
@@ -0,0 +1 @@
+{"jobs":[{"completedAt":"2026-05-07T13:14:32Z","conclusion":"success","databaseId":74822265631,"name":"Build Linux","startedAt":"2026-05-07T13:10:17Z","status":"completed","steps":[{"completedAt":"2026-05-07T13:10:19Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-05-07T13:10:18Z","status":"completed"},{"completedAt":"2026-05-07T13:10:20Z","conclusion":"success","name":"Run actions/checkout@v4","number":2,"startedAt":"2026-05-07T13:10:19Z","status":"completed"},{"completedAt":"2026-05-07T13:10:35Z","conclusion":"success","name":"Run subosito/flutter-action@v2","number":3,"startedAt":"2026-05-07T13:10:20Z","status":"completed"},{"completedAt":"2026-05-07T13:13:36Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-05-07T13:10:35Z","status":"completed"},{"completedAt":"2026-05-07T13:14:22Z","conclusion":"success","name":"Build Linux","number":5,"startedAt":"2026-05-07T13:13:36Z","status":"completed"},{"completedAt":"2026-05-07T13:14:25Z","conclusion":"success","name":"Package Linux","number":6,"startedAt":"2026-05-07T13:14:22Z","status":"completed"},{"completedAt":"2026-05-07T13:14:28Z","conclusion":"success","name":"Build AppImage","number":7,"startedAt":"2026-05-07T13:14:25Z","status":"completed"},{"completedAt":"2026-05-07T13:14:30Z","conclusion":"success","name":"Upload Linux Artifact","number":8,"startedAt":"2026-05-07T13:14:28Z","status":"completed"},{"completedAt":"2026-05-07T13:14:31Z","conclusion":"success","name":"Post Run subosito/flutter-action@v2","number":15,"startedAt":"2026-05-07T13:14:30Z","status":"completed"},{"completedAt":"2026-05-07T13:14:31Z","conclusion":"success","name":"Post Run actions/checkout@v4","number":16,"startedAt":"2026-05-07T13:14:31Z","status":"completed"},{"completedAt":"2026-05-07T13:14:31Z","conclusion":"success","name":"Complete job","number":17,"startedAt":"2026-05-07T13:14:31Z","status":"completed"}],"url":"https://github.com/abduznik/Freegosy/actions/runs/25497843287/job/74822265631"},{"completedAt":"2026-05-07T13:14:04Z","conclusion":"failure","databaseId":74822265637,"name":"Build Windows Installer","startedAt":"2026-05-07T13:10:16Z","status":"completed","steps":[{"completedAt":"2026-05-07T13:10:19Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-05-07T13:10:17Z","status":"completed"},{"completedAt":"2026-05-07T13:10:24Z","conclusion":"success","name":"Run actions/checkout@v4","number":2,"startedAt":"2026-05-07T13:10:19Z","status":"completed"},{"completedAt":"2026-05-07T13:12:08Z","conclusion":"success","name":"Run subosito/flutter-action@v2","number":3,"startedAt":"2026-05-07T13:10:24Z","status":"completed"},{"completedAt":"2026-05-07T13:12:18Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-05-07T13:12:08Z","status":"completed"},{"completedAt":"2026-05-07T13:13:59Z","conclusion":"success","name":"Build Windows Installer","number":5,"startedAt":"2026-05-07T13:12:18Z","status":"completed"},{"completedAt":"2026-05-07T13:14:00Z","conclusion":"failure","name":"Create Zip (Windows)","number":6,"startedAt":"2026-05-07T13:13:59Z","status":"completed"},{"completedAt":"2026-05-07T13:14:00Z","conclusion":"skipped","name":"Upload Windows Artifact","number":7,"startedAt":"2026-05-07T13:14:00Z","status":"completed"},{"completedAt":"2026-05-07T13:14:00Z","conclusion":"success","name":"Post Run subosito/flutter-action@v2","number":13,"startedAt":"2026-05-07T13:14:00Z","status":"completed"},{"completedAt":"2026-05-07T13:14:03Z","conclusion":"success","name":"Post Run actions/checkout@v4","number":14,"startedAt":"2026-05-07T13:14:00Z","status":"completed"},{"completedAt":"2026-05-07T13:14:03Z","conclusion":"success","name":"Complete job","number":15,"startedAt":"2026-05-07T13:14:03Z","status":"completed"}],"url":"https://github.com/abduznik/Freegosy/actions/runs/25497843287/job/74822265637"},{"completedAt":"2026-05-07T13:13:13Z","conclusion":"success","databaseId":74822265675,"name":"Build macOS DMG","startedAt":"2026-05-07T13:10:18Z","status":"completed","steps":[{"completedAt":"2026-05-07T13:10:20Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-05-07T13:10:18Z","status":"completed"},{"completedAt":"2026-05-07T13:10:21Z","conclusion":"success","name":"Run actions/checkout@v4","number":2,"startedAt":"2026-05-07T13:10:20Z","status":"completed"},{"completedAt":"2026-05-07T13:11:10Z","conclusion":"success","name":"Run subosito/flutter-action@v2","number":3,"startedAt":"2026-05-07T13:10:21Z","status":"completed"},{"completedAt":"2026-05-07T13:11:12Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-05-07T13:11:10Z","status":"completed"},{"completedAt":"2026-05-07T13:12:41Z","conclusion":"success","name":"Build macOS","number":5,"startedAt":"2026-05-07T13:11:12Z","status":"completed"},{"completedAt":"2026-05-07T13:12:42Z","conclusion":"success","name":"Ad-hoc sign macOS app","number":6,"startedAt":"2026-05-07T13:12:41Z","status":"completed"},{"completedAt":"2026-05-07T13:12:57Z","conclusion":"success","name":"Create DMG","number":7,"startedAt":"2026-05-07T13:12:42Z","status":"completed"},{"completedAt":"2026-05-07T13:13:04Z","conclusion":"success","name":"Create Zip (macOS)","number":8,"startedAt":"2026-05-07T13:12:57Z","status":"completed"},{"completedAt":"2026-05-07T13:13:09Z","conclusion":"success","name":"Upload macOS Artifact","number":9,"startedAt":"2026-05-07T13:13:04Z","status":"completed"},{"completedAt":"2026-05-07T13:13:10Z","conclusion":"success","name":"Post Run subosito/flutter-action@v2","number":17,"startedAt":"2026-05-07T13:13:09Z","status":"completed"},{"completedAt":"2026-05-07T13:13:10Z","conclusion":"success","name":"Post Run actions/checkout@v4","number":18,"startedAt":"2026-05-07T13:13:10Z","status":"completed"},{"completedAt":"2026-05-07T13:13:11Z","conclusion":"success","name":"Complete job","number":19,"startedAt":"2026-05-07T13:13:10Z","status":"completed"}],"url":"https://github.com/abduznik/Freegosy/actions/runs/25497843287/job/74822265675"},{"completedAt":"2026-05-07T13:14:47Z","conclusion":"failure","databaseId":74823055283,"name":"Create Release","startedAt":"2026-05-07T13:14:42Z","status":"completed","steps":[{"completedAt":"2026-05-07T13:14:44Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-05-07T13:14:43Z","status":"completed"},{"completedAt":"2026-05-07T13:14:44Z","conclusion":"success","name":"Create artifacts directory","number":2,"startedAt":"2026-05-07T13:14:44Z","status":"completed"},{"completedAt":"2026-05-07T13:14:45Z","conclusion":"success","name":"Download Linux Artifact","number":3,"startedAt":"2026-05-07T13:14:44Z","status":"completed"},{"completedAt":"2026-05-07T13:14:46Z","conclusion":"failure","name":"Download Windows Artifact","number":4,"startedAt":"2026-05-07T13:14:45Z","status":"completed"},{"completedAt":"2026-05-07T13:14:46Z","conclusion":"skipped","name":"Download macOS Artifact","number":5,"startedAt":"2026-05-07T13:14:46Z","status":"completed"},{"completedAt":"2026-05-07T13:14:46Z","conclusion":"skipped","name":"Create Release","number":6,"startedAt":"2026-05-07T13:14:46Z","status":"completed"},{"completedAt":"2026-05-07T13:14:46Z","conclusion":"success","name":"Complete job","number":7,"startedAt":"2026-05-07T13:14:46Z","status":"completed"}],"url":"https://github.com/abduznik/Freegosy/actions/runs/25497843287/job/74823055283"}]}
diff --git a/lib/core/emulator/linux_strategies/emudeck_strategy.dart b/lib/core/emulator/linux_strategies/emudeck_strategy.dart
index 21a76f1..35bff12 100644
--- a/lib/core/emulator/linux_strategies/emudeck_strategy.dart
+++ b/lib/core/emulator/linux_strategies/emudeck_strategy.dart
@@ -12,6 +12,7 @@ class EmuDeckStrategy extends LinuxEnvironmentStrategy {
 
   @override
   String getRomsRoot(String home, String? customPath, String? emudeckRoot) {
+    if (customPath != null) return customPath;
     if (emudeckRoot != null) {
       // 1. Check if user pointed directly to a 'roms' folder
       if (p.basename(emudeckRoot).toLowerCase() == 'roms') return emudeckRoot;
@@ -41,6 +42,7 @@ class EmuDeckStrategy extends LinuxEnvironmentStrategy {
 
   @override
   String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot) {
+    if (customPath != null) return customPath;
     if (emudeckRoot != null) {
       // 1. Check if user pointed directly to 'tools'
       if (p.basename(emudeckRoot).toLowerCase() == 'tools') return emudeckRoot;
diff --git a/lib/core/emulator/linux_strategies/retrodeck_strategy.dart b/lib/core/emulator/linux_strategies/retrodeck_strategy.dart
index 0ea2b31..caafaf6 100644
--- a/lib/core/emulator/linux_strategies/retrodeck_strategy.dart
+++ b/lib/core/emulator/linux_strategies/retrodeck_strategy.dart
@@ -12,12 +12,16 @@ class RetroDeckStrategy extends LinuxEnvironmentStrategy {
 
   @override
   String getRomsRoot(String home, String? customPath, String? emudeckRoot) {
-    return customPath ?? p.join(home, 'retrodeck', 'roms');
+    if (customPath != null) return customPath;
+    if (emudeckRoot != null) return p.join(emudeckRoot, 'roms');
+    return p.join(home, 'retrodeck', 'roms');
   }
 
   @override
   String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot) {
-    return customPath ?? p.join(home, 'retrodeck', 'tools');
+    if (customPath != null) return customPath;
+    if (emudeckRoot != null) return p.join(emudeckRoot, 'tools');
+    return p.join(home, 'retrodeck', 'tools');
   }
 
   @override
@@ -45,6 +49,7 @@ class RetroDeckStrategy extends LinuxEnvironmentStrategy {
 
   @override
   String getBiosPath(String home, String? emudeckRoot) {
+    if (emudeckRoot != null) return p.join(emudeckRoot, 'bios');
     return p.join(home, '.var', 'app', 'net.retrodeck.retrodeck', 'config', 'bios');
   }
 
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index 36d4588..ab53601 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -71,7 +71,7 @@ class DirectoryService {
   static const String _romsRootPathKey = 'romsRootPath';
   static const String _emulatorsRootPathKey = 'emulatorsRootPath';
   static const String _linuxSyncPresetKey = 'linuxSyncPreset';
-  static const String _emudeckRootPathKey = 'emudeckRootPath';
+  static const String _linuxPresetRootKey = 'emudeckRootPath'; // Keeping key name for compatibility
 
   // Known extensions per platform slug
   static const Map<String, List<String>> platformExtensions = {
@@ -111,7 +111,7 @@ class DirectoryService {
   late String romsRootPath;
   late String emulatorsRootPath;
   String linuxSyncPreset = 'default';
-  String? emudeckRootPath;
+  String? linuxPresetRootPath;
   final Map<String, String> _emulatorPathOverrides = {};
   StorageStatus status = const StorageStatus();
   
@@ -184,24 +184,24 @@ class DirectoryService {
   Future<StorageStatus> initialize() async {
     try {
       linuxSyncPreset = _prefs.getString(_linuxSyncPresetKey) ?? 'default';
-      emudeckRootPath = _prefs.getString(_emudeckRootPathKey);
+      linuxPresetRootPath = _prefs.getString(_linuxPresetRootKey);
       
       // Auto-detection logic for Linux
       if (defaultTargetPlatform == TargetPlatform.linux) {
         if (linuxSyncPreset == 'auto' || linuxSyncPreset == 'default') {
           // If we are in 'auto' or 'default', try to see if EmuDeck or RetroDeck is there.
           // BUT: If the user has already explicitly chosen a root, don't overwrite it with auto-detection.
-          if (emudeckRootPath == null) {
+          if (linuxPresetRootPath == null) {
             final detectedRoot = await detectEmuDeckRoot();
             if (detectedRoot != null) {
-              emudeckRootPath = detectedRoot;
+              linuxPresetRootPath = detectedRoot;
               linuxSyncPreset = 'emudeck';
               _linuxStrategy = EmuDeckStrategy();
               await _prefs.setString(_linuxSyncPresetKey, 'emudeck');
-              await _prefs.setString(_emudeckRootPathKey, emudeckRootPath!);
+              await _prefs.setString(_linuxPresetRootKey, linuxPresetRootPath!);
             }
           } else if (linuxSyncPreset == 'default') {
-             // If they are on 'default' (manual) but have an emudeckRootPath, 
+             // If they are on 'default' (manual) but have an linuxPresetRootPath, 
              // we stay on 'default' unless it's the very first run.
           }
           
@@ -228,13 +228,8 @@ class DirectoryService {
         final customRoms = _prefs.getString(_romsRootPathKey);
         final customEmus = _prefs.getString(_emulatorsRootPathKey);
         
-        // IMPORTANT: If we are using a preset (EmuDeck/RetroDeck), we IGNORE the manual custom paths
-        // to ensure the computed paths from the root always take precedence.
-        final String? effectiveCustomRoms = (linuxSyncPreset == 'default') ? customRoms : null;
-        final String? effectiveCustomEmus = (linuxSyncPreset == 'default') ? customEmus : null;
-
-        romsRootPath = activeLinuxEnvironment.getRomsRoot(home, effectiveCustomRoms, emudeckRootPath);
-        emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, effectiveCustomEmus, emudeckRootPath);
+        romsRootPath = activeLinuxEnvironment.getRomsRoot(home, customRoms, linuxPresetRootPath);
+        emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, customEmus, linuxPresetRootPath);
       } else {
         romsRootPath = _prefs.getString(_romsRootPathKey) ?? p.join(defaultBase, 'ROMs');
         emulatorsRootPath =
@@ -273,7 +268,7 @@ class DirectoryService {
     final home = io.Platform.environment['HOME'] ?? '';
 
     if (defaultTargetPlatform == TargetPlatform.linux) {
-      romsRootPath = activeLinuxEnvironment.getRomsRoot(home, null, emudeckRootPath);
+      romsRootPath = activeLinuxEnvironment.getRomsRoot(home, null, linuxPresetRootPath);
     } else {
       romsRootPath = '$base/ROMs';
       }
@@ -286,7 +281,7 @@ class DirectoryService {
       final home = io.Platform.environment['HOME'] ?? '';
 
       if (defaultTargetPlatform == TargetPlatform.linux) {
-        emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, null, emudeckRootPath);
+        emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, null, linuxPresetRootPath);
       } else {
         emulatorsRootPath = p.join(base, 'Emulators');
       }
@@ -299,13 +294,16 @@ class DirectoryService {
     await initialize();
   }
 
-  Future<void> setEmudeckRoot(String path) async {
-    // If user picked the 'Emulation' folder itself, go up one level
-    if (p.basename(path).toLowerCase() == 'emulation') {
-      path = p.dirname(path);
+  Future<void> setLinuxPresetRoot(String path) async {
+    // If user picked the 'Emulation' folder or 'retrodeck' folder itself, we might want to handle it
+    // But for now, we just store what they picked and strategies handle it.
+    final name = p.basename(path).toLowerCase();
+    if (name == 'emulation' || name == 'retrodeck') {
+      // If they picked the subfolder instead of the root, we can optionally go up
+      // but let's stay flexible for now.
     }
-    await _prefs.setString(_emudeckRootPathKey, path);
-    emudeckRootPath = path;
+    await _prefs.setString(_linuxPresetRootKey, path);
+    linuxPresetRootPath = path;
     // Re-initialize to update paths based on new root
     await initialize();
   }
@@ -718,7 +716,7 @@ class DirectoryService {
       return p.join(appData, emulatorName);
     } else if (io.Platform.isLinux) {
       final home = io.Platform.environment['HOME'] ?? '';
-      return activeLinuxEnvironment.getEmulatorAppSupportDirectory(home, emulatorName, emudeckRootPath, platformSlug: platformSlug);
+      return activeLinuxEnvironment.getEmulatorAppSupportDirectory(home, emulatorName, linuxPresetRootPath, platformSlug: platformSlug);
     }
     throw UnsupportedError('Platform not supported for save path resolution');
   }
@@ -726,7 +724,7 @@ class DirectoryService {
   Future<String> getEmulatorBiosDirectory(String emulatorId, {String? platformSlug}) async {
     if (io.Platform.isLinux) {
       final home = io.Platform.environment['HOME'] ?? '';
-      return activeLinuxEnvironment.getBiosPath(home, emudeckRootPath);
+      return activeLinuxEnvironment.getBiosPath(home, linuxPresetRootPath);
     }
     final emuDir = await getEmulatorDirectory(emulatorId);
     final dirPath = p.join(emuDir, 'BIOS');
@@ -776,7 +774,7 @@ class DirectoryService {
     }
 
     if (io.Platform.isLinux) {
-      final envPath = await activeLinuxEnvironment.findExecutable(emulatorId, executableName, emulatorsRootPath, emudeckRootPath);
+      final envPath = await activeLinuxEnvironment.findExecutable(emulatorId, executableName, emulatorsRootPath, linuxPresetRootPath);
       if (envPath != null) return envPath;
     }
 
diff --git a/lib/ui/screens/library_actions.dart b/lib/ui/screens/library_actions.dart
index 754c2d2..d28531e 100644
--- a/lib/ui/screens/library_actions.dart
+++ b/lib/ui/screens/library_actions.dart
@@ -351,9 +351,10 @@ mixin LibraryActionsMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
 
             ErrorHandler.showInfo(context, 'Syncing', message: 'Auto-syncing saves...');
 
+            bool ok = false;
             if (syncService != null) {
               final syncMode = ref.read(retroarchSyncModeProvider);
-              final ok = await syncService.pushSaves(game, romPath, syncMode: syncMode);
+              ok = await syncService.pushSaves(game, romPath, syncMode: syncMode);
 
               // --- Safety Sandwich: post-exit backup if save changed ---
               try {
@@ -383,7 +384,7 @@ mixin LibraryActionsMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
             if (!context.mounted) return;
             if (ok) {
               ErrorHandler.showSuccess(context, 'Save Synced', message: 'Saves synced');
-            } else {
+            } else if (syncService != null) {
               ErrorHandler.showSuccess(context, 'Up to Date', message: 'No files to upload');
             }
           } catch (e) {
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index a605be0..80377f5 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -260,7 +260,6 @@ 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: [
@@ -292,67 +291,46 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
       if (io.Platform.isLinux && (preset == 'emudeck' || preset == 'retrodeck')) ...[
         _buildPathRow(
           label: '${preset == 'emudeck' ? 'EmuDeck' : 'RetroDeck'} Installation Root',
-          currentPath: preset == 'emudeck' 
-              ? (directoryService.emudeckRootPath ?? 'Not set') 
-              : (prefs.getString('retrodeckRootPath') ?? 'Not set'),
+          currentPath: directoryService.linuxPresetRootPath ?? 'Not set',
           onChanged: (p) async { 
             if (p != null) { 
-              if (preset == 'emudeck') {
-                await directoryService.setEmudeckRoot(p);
-              } else {
-                await prefs.setString('retrodeckRootPath', p);
-                await directoryService.initialize();
-              }
+              await directoryService.setLinuxPresetRoot(p);
               ref.invalidate(directoryServiceProvider); 
             } 
           },
         ),
         const SizedBox(height: 16),
-        const Text('Computed Paths (Read-only)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey)),
-        const SizedBox(height: 8),
-        _buildPathRow(
-          label: 'ROMs Directory',
-          currentPath: directoryService.romsRootPath,
-          onChanged: null,
-        ),
-        const SizedBox(height: 12),
-        _buildPathRow(
-          label: 'Emulators Directory',
-          currentPath: directoryService.emulatorsRootPath,
-          onChanged: null,
-        ),
-      ] else ...[
-        // This handles both non-Linux OSs and Linux 'Manual' mode
-        _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(); 
+      ],
+
+      _buildPathRow(
+        label: 'ROMs Directory',
+        currentPath: directoryService.romsRootPath,
+        onChanged: (p) async { 
+          if (p != null) { 
+            await directoryService.setRomsRoot(p); 
             ref.invalidate(directoryServiceProvider); 
-          },
-        ),
-        const SizedBox(height: 16),
-        _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(); 
+          } 
+        },
+        onReset: () async { 
+          await directoryService.resetRomsRoot(); 
+          ref.invalidate(directoryServiceProvider); 
+        },
+      ),
+      const SizedBox(height: 16),
+      _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),
diff --git a/test/core/emulator/linux_strategies_test.dart b/test/core/emulator/linux_strategies_test.dart
index 3b05502..077a46b 100644
--- a/test/core/emulator/linux_strategies_test.dart
+++ b/test/core/emulator/linux_strategies_test.dart
@@ -81,6 +81,18 @@ void main() {
         final result = await emudeckStrategy.findExecutable('cemu', 'Cemu.AppImage', tempHome.path, tempHome.path);
         expect(result, p.join(tempHome.path, 'Emulation', 'tools', 'launchers', 'cemu.sh'));
       });
+
+      test('Respects custom ROMs path even if EmuDeck root is set', () {
+        final custom = p.join(tempHome.path, 'MyCustomROMs');
+        final result = emudeckStrategy.getRomsRoot(tempHome.path, custom, tempHome.path);
+        expect(result, custom);
+      });
+
+      test('Respects custom Emulators path even if EmuDeck root is set', () {
+        final custom = p.join(tempHome.path, 'MyCustomTools');
+        final result = emudeckStrategy.getEmulatorsRoot(tempHome.path, custom, tempHome.path);
+        expect(result, custom);
+      });
     });
 
     group('RetroDeckStrategy', () {
@@ -101,6 +113,24 @@ void main() {
         final result = retrodeckStrategy.getEmulatorAppSupportDirectory(tempHome.path, 'pcsx2', null);
         expect(result, p.join(retrodeckVarConfig.path, 'PCSX2', 'saves'));
       });
+
+      test('Respects custom ROMs path', () {
+        final custom = p.join(tempHome.path, 'MyRetroROMs');
+        final result = retrodeckStrategy.getRomsRoot(tempHome.path, custom, null);
+        expect(result, custom);
+      });
+
+      test('Resolves ROMs relative to installation root (e.g. SD Card)', () {
+        final sdCardRoot = '/run/media/deck/SDCARD/retrodeck';
+        final result = retrodeckStrategy.getRomsRoot(tempHome.path, null, sdCardRoot);
+        expect(result, p.join(sdCardRoot, 'roms'));
+      });
+
+      test('Resolves BIOS relative to installation root (e.g. SD Card)', () {
+        final sdCardRoot = '/run/media/deck/SDCARD/retrodeck';
+        final result = retrodeckStrategy.getBiosPath(tempHome.path, sdCardRoot);
+        expect(result, p.join(sdCardRoot, 'bios'));
+      });
     });
   });
 }
diff --git a/test/unit/firmware_service_test.mocks.dart b/test/unit/firmware_service_test.mocks.dart
index 22ecb61..557a29e 100644
--- a/test/unit/firmware_service_test.mocks.dart
+++ b/test/unit/firmware_service_test.mocks.dart
@@ -511,8 +511,8 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
   );
 
   @override
-  set emudeckRootPath(String? value) => super.noSuchMethod(
-    Invocation.setter(#emudeckRootPath, value),
+  set linuxPresetRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#linuxPresetRootPath, value),
     returnValueForMissingStub: null,
   );
 
@@ -581,9 +581,9 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
           as _i8.Future<void>);
 
   @override
-  _i8.Future<void> setEmudeckRoot(String? path) =>
+  _i8.Future<void> setLinuxPresetRoot(String? path) =>
       (super.noSuchMethod(
-            Invocation.method(#setEmudeckRoot, [path]),
+            Invocation.method(#setLinuxPresetRoot, [path]),
             returnValue: _i8.Future<void>.value(),
             returnValueForMissingStub: _i8.Future<void>.value(),
           )
diff --git a/test/unit/save_sync_service_test.mocks.dart b/test/unit/save_sync_service_test.mocks.dart
index 923c94c..376ddbd 100644
--- a/test/unit/save_sync_service_test.mocks.dart
+++ b/test/unit/save_sync_service_test.mocks.dart
@@ -511,8 +511,8 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
   );
 
   @override
-  set emudeckRootPath(String? value) => super.noSuchMethod(
-    Invocation.setter(#emudeckRootPath, value),
+  set linuxPresetRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#linuxPresetRootPath, value),
     returnValueForMissingStub: null,
   );
 
@@ -581,9 +581,9 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
           as _i8.Future<void>);
 
   @override
-  _i8.Future<void> setEmudeckRoot(String? path) =>
+  _i8.Future<void> setLinuxPresetRoot(String? path) =>
       (super.noSuchMethod(
-            Invocation.method(#setEmudeckRoot, [path]),
+            Invocation.method(#setLinuxPresetRoot, [path]),
             returnValue: _i8.Future<void>.value(),
             returnValueForMissingStub: _i8.Future<void>.value(),
           )
diff --git a/test/unit/strategy_registry_test.mocks.dart b/test/unit/strategy_registry_test.mocks.dart
index e58387b..7e725a9 100644
--- a/test/unit/strategy_registry_test.mocks.dart
+++ b/test/unit/strategy_registry_test.mocks.dart
@@ -123,8 +123,8 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
   );
 
   @override
-  set emudeckRootPath(String? value) => super.noSuchMethod(
-    Invocation.setter(#emudeckRootPath, value),
+  set linuxPresetRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#linuxPresetRootPath, value),
     returnValueForMissingStub: null,
   );
 
@@ -193,9 +193,9 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
           as _i5.Future<void>);
 
   @override
-  _i5.Future<void> setEmudeckRoot(String? path) =>
+  _i5.Future<void> setLinuxPresetRoot(String? path) =>
       (super.noSuchMethod(
-            Invocation.method(#setEmudeckRoot, [path]),
+            Invocation.method(#setLinuxPresetRoot, [path]),
             returnValue: _i5.Future<void>.value(),
             returnValueForMissingStub: _i5.Future<void>.value(),
           )
diff --git a/test/widgets/library_screen_test.mocks.dart b/test/widgets/library_screen_test.mocks.dart
index f56efbf..7e53e1b 100644
--- a/test/widgets/library_screen_test.mocks.dart
+++ b/test/widgets/library_screen_test.mocks.dart
@@ -512,8 +512,8 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
   );
 
   @override
-  set emudeckRootPath(String? value) => super.noSuchMethod(
-    Invocation.setter(#emudeckRootPath, value),
+  set linuxPresetRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#linuxPresetRootPath, value),
     returnValueForMissingStub: null,
   );
 
@@ -582,9 +582,9 @@ class MockDirectoryService extends _i1.Mock implements _i4.DirectoryService {
           as _i8.Future<void>);
 
   @override
-  _i8.Future<void> setEmudeckRoot(String? path) =>
+  _i8.Future<void> setLinuxPresetRoot(String? path) =>
       (super.noSuchMethod(
-            Invocation.method(#setEmudeckRoot, [path]),
+            Invocation.method(#setLinuxPresetRoot, [path]),
             returnValue: _i8.Future<void>.value(),
             returnValueForMissingStub: _i8.Future<void>.value(),
           )
diff --git a/test/widgets/settings_screen_test.mocks.dart b/test/widgets/settings_screen_test.mocks.dart
index 85d5435..e112d81 100644
--- a/test/widgets/settings_screen_test.mocks.dart
+++ b/test/widgets/settings_screen_test.mocks.dart
@@ -140,8 +140,8 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
   );
 
   @override
-  set emudeckRootPath(String? value) => super.noSuchMethod(
-    Invocation.setter(#emudeckRootPath, value),
+  set linuxPresetRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#linuxPresetRootPath, value),
     returnValueForMissingStub: null,
   );
 
@@ -210,9 +210,9 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
           as _i7.Future<void>);
 
   @override
-  _i7.Future<void> setEmudeckRoot(String? path) =>
+  _i7.Future<void> setLinuxPresetRoot(String? path) =>
       (super.noSuchMethod(
-            Invocation.method(#setEmudeckRoot, [path]),
+            Invocation.method(#setLinuxPresetRoot, [path]),
             returnValue: _i7.Future<void>.value(),
             returnValueForMissingStub: _i7.Future<void>.value(),
           )

Clone this wiki locally