Skip to content

commit f83ad34

abduznik edited this page May 23, 2026 · 1 revision

feat: Standardized BIOS Management from RomM (#12)

Commit: f83ad343807a5fd932065888a8bad581d285ff1d

Author: abduznik

Date: 2026-04-10

Message

  • feat: implement scrollable screenshot gallery using PageView in fullscreen dialog

  • Chore:add scrollable screenshot view

  • feat: implement scrollable screenshot gallery using PageView in fullscreen dialog

  • Chore:add scrollable screenshot view

  • feat: implement interactive screenshot gallery with swipe and zoom closes #9

  • fix: resolve deprecated scale usage in ScreenshotGalleryDialog

  • feat: add Firmware model and support for firmware fetching/downloading in RommService

  • feat: simplify BIOS path convention to /EMULATOR_NAME/BIOS and add FirmwareServiceProvider

  • feat: implement FirmwareService for managing BIOS downloads and placement

  • feat: add Sync BIOS from RomM button to emulator settings

  • test: add unit tests for FirmwareService and update project mocks

  • feat: add onProgress parameter to downloadFirmware in RommService

  • feat: add progress reporting and syncFirmwareForEmulator to FirmwareService

  • feat: add per-emulator BIOS sync button and robust progress dialog

  • test: update firmware tests for onProgress and regenerate project mocks

  • chore: remove unused github command templates


Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

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

Files Changed

lib/core/emulator/firmware_service.dart            | 114 ++++
 lib/core/romm/romm_models.dart                     |  59 ++
 lib/core/romm/romm_service.dart                    |  46 ++
 lib/core/storage/directory_service.dart            |  21 +-
 lib/providers/romm_provider.dart                   |  10 +
 lib/ui/screens/settings_emulators_section.dart     | 138 ++++-
 test/unit/firmware_service_test.dart               | 106 ++++
 test/unit/firmware_service_test.mocks.dart         | 658 +++++++++++++++++++++
 test/unit/paginated_games_provider_test.mocks.dart |  76 +++
 test/unit/save_sync_service_test.mocks.dart        |  99 ++++
 test/unit/strategy_registry_test.mocks.dart        |  23 +
 test/widgets/library_screen_test.mocks.dart        |  99 ++++
 test/widgets/settings_screen_test.mocks.dart       |  99 ++++
 13 files changed, 1532 insertions(+), 16 deletions(-)
  • lib/core/emulator/firmware_service.dart
  • lib/core/romm/romm_models.dart
  • lib/core/romm/romm_service.dart
  • lib/core/storage/directory_service.dart
  • lib/providers/romm_provider.dart
  • lib/ui/screens/settings_emulators_section.dart
  • test/unit/firmware_service_test.dart
  • test/unit/firmware_service_test.mocks.dart
  • test/unit/paginated_games_provider_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/lib/core/emulator/firmware_service.dart b/lib/core/emulator/firmware_service.dart
new file mode 100644
index 0000000..011bdc6
--- /dev/null
+++ b/lib/core/emulator/firmware_service.dart
@@ -0,0 +1,114 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/romm/romm_service.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+import 'package:freegosy/core/emulator/strategy_registry.dart';
+
+typedef FirmwareProgressCallback = void Function(String fileName, int received, int total);
+
+class FirmwareService {
+  final RommService _rommService;
+  final DirectoryService _directoryService;
+  final StrategyRegistry _strategyRegistry;
+
+  FirmwareService(this._rommService, this._directoryService, this._strategyRegistry);
+
+  /// Downloads all available firmware for all platforms and places them in the appropriate emulator BIOS directories.
+  Future<void> syncAllFirmware({FirmwareProgressCallback? onProgress}) async {
+    try {
+      final platforms = await _rommService.getPlatforms();
+      for (final platform in platforms) {
+        if (platform.firmware.isEmpty) continue;
+
+        final strategy = _strategyRegistry.getStrategyForSlug(platform.slug);
+        if (strategy == null) {
+          debugPrint('[FirmwareService] No emulator found for platform: ${platform.slug}');
+          continue;
+        }
+
+        final biosDir = await _directoryService.getEmulatorBiosDirectory(strategy.emulatorId);
+        
+        for (final firmware in platform.firmware) {
+          await _downloadAndPlaceFirmware(firmware, biosDir, onProgress: onProgress);
+        }
+      }
+    } catch (e) {
+      debugPrint('[FirmwareService] Error syncing firmware: $e');
+    }
+  }
+
+  /// Downloads firmware for a specific platform and places it in the emulator's BIOS directory.
+  Future<void> syncFirmwareForPlatform(String platformSlug, {FirmwareProgressCallback? onProgress}) async {
+    try {
+      final platforms = await _rommService.getPlatforms();
+      final platform = platforms.firstWhere((p) => p.slug == platformSlug, orElse: () => throw Exception('Platform not found: $platformSlug'));
+      
+      if (platform.firmware.isEmpty) return;
+
+      final strategy = _strategyRegistry.getStrategyForSlug(platform.slug);
+      if (strategy == null) {
+        debugPrint('[FirmwareService] No emulator found for platform: ${platform.slug}');
+        return;
+      }
+
+      final biosDir = await _directoryService.getEmulatorBiosDirectory(strategy.emulatorId);
+      
+      for (final firmware in platform.firmware) {
+        await _downloadAndPlaceFirmware(firmware, biosDir, onProgress: onProgress);
+      }
+    } catch (e) {
+      debugPrint('[FirmwareService] Error syncing firmware for $platformSlug: $e');
+    }
+  }
+
+  /// Downloads firmware for a specific emulator and places it in its BIOS directory.
+  Future<void> syncFirmwareForEmulator(String emulatorId, {FirmwareProgressCallback? onProgress}) async {
+    try {
+      final platforms = await _rommService.getPlatforms();
+      final biosDir = await _directoryService.getEmulatorBiosDirectory(emulatorId);
+
+      for (final platform in platforms) {
+        final strategy = _strategyRegistry.getStrategyForSlug(platform.slug);
+        if (strategy?.emulatorId == emulatorId) {
+          if (platform.firmware.isEmpty) continue;
+          for (final firmware in platform.firmware) {
+            await _downloadAndPlaceFirmware(firmware, biosDir, onProgress: onProgress);
+          }
+        }
+      }
+    } catch (e) {
+      debugPrint('[FirmwareService] Error syncing firmware for emulator $emulatorId: $e');
+    }
+  }
+
+  Future<void> _downloadAndPlaceFirmware(Firmware firmware, String biosDir, {FirmwareProgressCallback? onProgress}) async {
+    final destPath = p.join(biosDir, firmware.fileName);
+    final destFile = File(destPath);
+
+    if (await destFile.exists()) {
+      debugPrint('[FirmwareService] Firmware already exists: ${firmware.fileName}');
+      return;
+    }
+
+    debugPrint('[FirmwareService] Downloading firmware: ${firmware.fileName} to $destPath');
+    
+    // Initial progress report
+    onProgress?.call(firmware.fileName, 0, firmware.fileSizeBytes);
+
+    final bytes = await _rommService.downloadFirmware(
+      firmware, 
+      onProgress: (received, total) {
+        onProgress?.call(firmware.fileName, received, total);
+      }
+    );
+
+    if (bytes != null) {
+      await destFile.writeAsBytes(bytes);
+      debugPrint('[FirmwareService] Successfully saved firmware: ${firmware.fileName}');
+    } else {
+      debugPrint('[FirmwareService] Failed to download firmware: ${firmware.fileName}');
+    }
+  }
+}
diff --git a/lib/core/romm/romm_models.dart b/lib/core/romm/romm_models.dart
index 24bc27f..aa39f5b 100644
--- a/lib/core/romm/romm_models.dart
+++ b/lib/core/romm/romm_models.dart
@@ -152,6 +152,59 @@ class Game {
   }
 }
 
+class Firmware {
+  final int id;
+  final String fileName;
+  final String? fileNameNoTags;
+  final String? fileNameNoExt;
+  final String? fileExtension;
+  final String? filePath;
+  final int fileSizeBytes;
+  final bool isVerified;
+  final String? crcHash;
+  final String? md5Hash;
+  final String? sha1Hash;
+  final bool missingFromFs;
+  final DateTime? createdAt;
+  final DateTime? updatedAt;
+
+  Firmware({
+    required this.id,
+    required this.fileName,
+    this.fileNameNoTags,
+    this.fileNameNoExt,
+    this.fileExtension,
+    this.filePath,
+    required this.fileSizeBytes,
+    this.isVerified = false,
+    this.crcHash,
+    this.md5Hash,
+    this.sha1Hash,
+    this.missingFromFs = false,
+    this.createdAt,
+    this.updatedAt,
+  });
+
+  factory Firmware.fromJson(Map<String, dynamic> json) {
+    return Firmware(
+      id: json['id'] as int? ?? 0,
+      fileName: json['file_name']?.toString() ?? '',
+      fileNameNoTags: json['file_name_no_tags']?.toString(),
+      fileNameNoExt: json['file_name_no_ext']?.toString(),
+      fileExtension: json['file_extension']?.toString(),
+      filePath: json['file_path']?.toString(),
+      fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
+      isVerified: json['is_verified'] as bool? ?? false,
+      crcHash: json['crc_hash']?.toString(),
+      md5Hash: json['md5_hash']?.toString(),
+      sha1Hash: json['sha1_hash']?.toString(),
+      missingFromFs: json['missing_from_fs'] as bool? ?? false,
+      createdAt: json['created_at'] != null ? DateTime.tryParse(json['created_at'].toString()) : null,
+      updatedAt: json['updated_at'] != null ? DateTime.tryParse(json['updated_at'].toString()) : null,
+    );
+  }
+}
+
 class Platform {
   final int id;
   final String name;
@@ -159,6 +212,8 @@ class Platform {
   final String fsSlug;
   final String displayName;
   final int gamesCount;
+  final List<Firmware> firmware;
+  final int firmwareCount;
 
   Platform({
     required this.id,
@@ -167,6 +222,8 @@ class Platform {
     this.fsSlug = '',
     this.displayName = '',
     this.gamesCount = 0,
+    this.firmware = const [],
+    this.firmwareCount = 0,
   });
 
   factory Platform.fromJson(Map<String, dynamic> json) {
@@ -179,6 +236,8 @@ class Platform {
       gamesCount: (json['rom_count'] as int?) ?? 
                   (json['roms_count'] as int?) ?? 
                   (json['games_count'] as int?) ?? 0,
+      firmware: (json['firmware'] as List<dynamic>?)?.map((e) => Firmware.fromJson(e)).toList() ?? [],
+      firmwareCount: json['firmware_count'] as int? ?? 0,
     );
   }
 }
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index 2818871..33afc9a 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -579,6 +579,52 @@ class RommService {
     }
   }
 
+  Future<List<Firmware>> getFirmware({String? platformId}) async {
+    final params = <String, dynamic>{};
+    if (platformId != null) {
+      params['platform_id'] = platformId;
+    }
+    final response = await _dio.get('/api/firmware', queryParameters: params, options: _authOptions);
+    if (response.statusCode == 200) {
+      final List<dynamic> items;
+      if (response.data is Map && response.data.containsKey('items')) {
+        items = response.data['items'] as List<dynamic>;
+      } else {
+        items = response.data as List<dynamic>;
+      }
+      return items.map((item) => Firmware.fromJson(item)).toList();
+    }
+    throw DioException(
+      requestOptions: response.requestOptions,
+      response: response,
+      type: DioExceptionType.badResponse,
+    );
+  }
+
+  String getFirmwareDownloadUrl(Firmware firmware) {
+    final baseUrl = _normalizeBaseUrl(config.baseUrl);
+    return '$baseUrl/api/firmware/${firmware.id}/content/${Uri.encodeComponent(firmware.fileName)}';
+  }
+
+  Future<Uint8List?> downloadFirmware(Firmware firmware, {void Function(int received, int total)? onProgress}) async {
+    try {
+      final url = getFirmwareDownloadUrl(firmware);
+      final opts = _authOptions.copyWith(responseType: ResponseType.bytes);
+      final response = await _dio.get<List<int>>(
+        url,
+        options: opts,
+        onReceiveProgress: onProgress,
+      );
+      if (response.statusCode == 200 && response.data != null) {
+        return Uint8List.fromList(response.data!);
+      }
+      return null;
+    } catch (e) {
+      debugPrint("ERROR in downloadFirmware: $e");
+      return null;
+    }
+  }
+
   Future<bool> updateRomProps(
     String romId, {
     bool? backlogged,
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index 257736f..eb665cc 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -296,24 +296,17 @@ class DirectoryService {
     throw UnsupportedError('Platform not supported for save path resolution');
   }
 
-  Future<String> getEmulatorSystemDirectory(String emulatorId) async {
-    if (emulatorId == 'retroarch') {
-      final emuDir = await getEmulatorDirectory(emulatorId);
-      final dirPath = p.join(emuDir, 'system');
-      await _ensureDirectoryExists(dirPath);
-      return dirPath;
-    }
-
-    if (emulatorId == 'azahar' || emulatorId == 'pcsx2') {
-      return await getEmulatorAppSupportDirectory(
-          emulatorId == 'azahar' ? 'Azahar' : 'PCSX2');
-    }
-
-    final dirPath = await getEmulatorDirectory(emulatorId);
+  Future<String> getEmulatorBiosDirectory(String emulatorId) async {
+    final emuDir = await getEmulatorDirectory(emulatorId);
+    final dirPath = p.join(emuDir, 'BIOS');
     await _ensureDirectoryExists(dirPath);
     return dirPath;
   }
 
+  Future<String> getEmulatorSystemDirectory(String emulatorId) async {
+    return await getEmulatorBiosDirectory(emulatorId);
+  }
+
   Future<void> deleteEmulator(String emulatorId) async {
     final dirPath = await getEmulatorDirectory(emulatorId);
     final directory = io.Directory(dirPath);
diff --git a/lib/providers/romm_provider.dart b/lib/providers/romm_provider.dart
index 161b4d2..f95d43f 100644
--- a/lib/providers/romm_provider.dart
+++ b/lib/providers/romm_provider.dart
@@ -10,8 +10,18 @@ import 'package:freegosy/core/emulator/strategies/windows_strategy.dart';
 
 import 'package:freegosy/core/storage/download_cache_service.dart';
 
+import 'package:freegosy/core/emulator/firmware_service.dart';
+
 final _secureStorage = const FlutterSecureStorage();
 
+final firmwareServiceProvider = FutureProvider<FirmwareService?>((ref) async {
+  final rommService = ref.watch(rommServiceProvider);
+  final directoryService = ref.watch(directoryServiceProvider).asData?.value;
+  final strategyRegistry = await ref.watch(strategyRegistryProvider.future);
+  if (rommService == null || directoryService == null || strategyRegistry == null) return null;
+  return FirmwareService(rommService, directoryService, strategyRegistry);
+});
+
 final downloadCacheServiceProvider = Provider<DownloadCacheService>((ref) {
   return DownloadCacheService();
 });
diff --git a/lib/ui/screens/settings_emulators_section.dart b/lib/ui/screens/settings_emulators_section.dart
index c6e27a5..8cbeb23 100644
--- a/lib/ui/screens/settings_emulators_section.dart
+++ b/lib/ui/screens/settings_emulators_section.dart
@@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
 import '../../core/storage/directory_service.dart';
 import '../../core/emulator/emulator_registry_data.dart';
 import '../../core/emulator/strategy_registry.dart';
+import '../../core/emulator/firmware_service.dart';
 import '../../providers/download_provider.dart';
 import '../../providers/romm_provider.dart';
 
@@ -31,8 +32,20 @@ Widget buildEmulatorsSection(
   return Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
-      const Text('Emulators',
-          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
+      Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          const Text('Emulators',
+              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
+          ElevatedButton.icon(
+            icon: const Icon(Icons.sync),
+            label: const Text('Sync BIOS from RomM'),
+            onPressed: () async {
+              _syncAllBios(context, ref);
+            },
+          ),
+        ],
+      ),
       const SizedBox(height: 16),
       if (!emulatorsLoaded)
         const Center(child: CircularProgressIndicator())
@@ -79,6 +92,13 @@ Widget buildEmulatorsSection(
                     }
                   },
                 ),
+                IconButton(
+                  icon: const Icon(Icons.library_books, size: 20),
+                  tooltip: 'Sync BIOS from RomM',
+                  onPressed: () async {
+                    _syncBiosForEmulator(context, ref, emulatorId, emulatorName);
+                  },
+                ),
                 if (isInstalled) ...[
                   IconButton(
                     icon: const Icon(Icons.play_arrow, color: Colors.green),
@@ -260,3 +280,117 @@ Widget buildConflictsSection(
     ],
   );
 }
+
+void _syncAllBios(BuildContext context, WidgetRef ref) async {
+  final firmwareService = await ref.read(firmwareServiceProvider.future);
+  if (firmwareService == null) return;
+
+  if (!context.mounted) return;
+
+  showDialog(
+    context: context,
+    barrierDismissible: false,
+    builder: (context) => _FirmwareProgressDialog(
+      title: 'Syncing All BIOS',
+      onSync: (onProgress) => firmwareService.syncAllFirmware(onProgress: onProgress),
+    ),
+  );
+}
+
+void _syncBiosForEmulator(BuildContext context, WidgetRef ref, String emulatorId, String emulatorName) async {
+  final firmwareService = await ref.read(firmwareServiceProvider.future);
+  if (firmwareService == null) return;
+
+  if (!context.mounted) return;
+
+  showDialog(
+    context: context,
+    barrierDismissible: false,
+    builder: (context) => _FirmwareProgressDialog(
+      title: 'Syncing BIOS for $emulatorName',
+      onSync: (onProgress) => firmwareService.syncFirmwareForEmulator(emulatorId, onProgress: onProgress),
+    ),
+  );
+}
+
+class _FirmwareProgressDialog extends StatefulWidget {
+  final String title;
+  final Future<void> Function(FirmwareProgressCallback onProgress) onSync;
+
+  const _FirmwareProgressDialog({
+    required this.title,
+    required this.onSync,
+  });
+
+  @override
+  State<_FirmwareProgressDialog> createState() => _FirmwareProgressDialogState();
+}
+
+class _FirmwareProgressDialogState extends State<_FirmwareProgressDialog> {
+  String _currentFile = 'Initializing...';
+  double _progress = 0;
+  String _status = '';
+  bool _isComplete = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _startSync();
+  }
+
+  void _startSync() async {
+    await widget.onSync((fileName, received, total) {
+      if (mounted) {
+        setState(() {
+          _currentFile = fileName;
+          if (total > 0) {
+            _progress = received / total;
+            final mbReceived = (received / (1024 * 1024)).toStringAsFixed(1);
+            final mbTotal = (total / (1024 * 1024)).toStringAsFixed(1);
+            
+            if (total > 10 * 1024 * 1024) {
+              _status = 'Downloading large file... ($mbReceived / $mbTotal MB)';
+            } else {
+              _status = '$mbReceived / $mbTotal MB';
+            }
+          } else {
+            _progress = 0;
+            _status = 'Fetching...';
+          }
+        });
+      }
+    });
+
+    if (mounted) {
+      setState(() {
+        _isComplete = true;
+        _currentFile = 'Complete!';
+        _status = 'All BIOS files synced successfully.';
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text(widget.title),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(_currentFile, style: const TextStyle(fontWeight: FontWeight.bold)),
+          const SizedBox(height: 8),
+          LinearProgressIndicator(value: _isComplete ? 1.0 : (_progress > 0 ? _progress : null)),
+          const SizedBox(height: 8),
+          Text(_status, style: const TextStyle(fontSize: 12, color: Colors.grey)),
+        ],
+      ),
+      actions: [
+        TextButton(
+          onPressed: _isComplete ? () => Navigator.pop(context) : null,
+          child: Text(_isComplete ? 'Close' : 'Please wait...'),
+        ),
+      ],
+    );
+  }
+}
diff --git a/test/unit/firmware_service_test.dart b/test/unit/firmware_service_test.dart
new file mode 100644
index 0000000..3cc04a5
--- /dev/null
+++ b/test/unit/firmware_service_test.dart
@@ -0,0 +1,106 @@
+import 'dart:io';
+import 'dart:typed_data';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/romm/romm_service.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+import 'package:freegosy/core/emulator/strategy_registry.dart';
+import 'package:freegosy/core/emulator/firmware_service.dart';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+
+import 'firmware_service_test.mocks.dart';
+
+class MockEmulatorStrategy extends Mock implements EmulatorStrategy {
+  @override
+  String get emulatorId => 'test_emulator';
+}
+
+@GenerateMocks([RommService, DirectoryService, StrategyRegistry])
+void main() {
+  late FirmwareService service;
+  late MockRommService mockRommService;
+  late MockDirectoryService mockDirectoryService;
+  late MockStrategyRegistry mockStrategyRegistry;
+
+  setUp(() {
+    mockRommService = MockRommService();
+    mockDirectoryService = MockDirectoryService();
+    mockStrategyRegistry = MockStrategyRegistry();
+    service = FirmwareService(mockRommService, mockDirectoryService, mockStrategyRegistry);
+  });
+
+  group('FirmwareService', () {
+    test('syncAllFirmware() downloads and places firmware correctly', () async {
+      final tempDir = await Directory.systemTemp.createTemp('firmware_test');
+      final biosDir = p.join(tempDir.path, 'BIOS');
+      await Directory(biosDir).create();
+
+      final firmware = Firmware(
+        id: 1,
+        fileName: 'test_bios.bin',
+        fileSizeBytes: 100,
+      );
+
+      final platform = Platform(
+        id: 1,
+        name: 'Test Platform',
+        slug: 'test_platform',
+        firmware: [firmware],
+      );
+
+      final mockStrategy = MockEmulatorStrategy();
+
+      when(mockRommService.getPlatforms()).thenAnswer((_) async => [platform]);
+      when(mockStrategyRegistry.getStrategyForSlug('test_platform')).thenReturn(mockStrategy);
+      when(mockDirectoryService.getEmulatorBiosDirectory('test_emulator')).thenAnswer((_) async => biosDir);
+      when(mockRommService.downloadFirmware(firmware, onProgress: anyNamed('onProgress')))
+          .thenAnswer((_) async => Uint8List.fromList([1, 2, 3]));
+
+      await service.syncAllFirmware();
+
+      final destFile = File(p.join(biosDir, 'test_bios.bin'));
+      expect(await destFile.exists(), isTrue);
+      expect(await destFile.readAsBytes(), equals([1, 2, 3]));
+
+      await tempDir.delete(recursive: true);
+    });
+
+    test('syncFirmwareForPlatform() syncs specifically for one platform', () async {
+       final tempDir = await Directory.systemTemp.createTemp('firmware_test_single');
+      final biosDir = p.join(tempDir.path, 'BIOS');
+      await Directory(biosDir).create();
+
+      final firmware = Firmware(
+        id: 2,
+        fileName: 'platform_bios.bin',
+        fileSizeBytes: 200,
+      );
+
+      final platform = Platform(
+        id: 2,
+        name: 'Single Platform',
+        slug: 'single_slug',
+        firmware: [firmware],
+      );
+
+      final mockStrategy = MockEmulatorStrategy();
+
+      when(mockRommService.getPlatforms()).thenAnswer((_) async => [platform]);
+      when(mockStrategyRegistry.getStrategyForSlug('single_slug')).thenReturn(mockStrategy);
+      when(mockDirectoryService.getEmulatorBiosDirectory('test_emulator')).thenAnswer((_) async => biosDir);
+      when(mockRommService.downloadFirmware(firmware, onProgress: anyNamed('onProgress')))
+          .thenAnswer((_) async => Uint8List.fromList([4, 5, 6]));
+
+      await service.syncFirmwareForPlatform('single_slug');
+
+      final destFile = File(p.join(biosDir, 'platform_bios.bin'));
+      expect(await destFile.exists(), isTrue);
+      expect(await destFile.readAsBytes(), equals([4, 5, 6]));
+
+      await tempDir.delete(recursive: true);
+    });
+  });
+}
diff --git a/test/unit/firmware_service_test.mocks.dart b/test/unit/firmware_service_test.mocks.dart
new file mode 100644
index 0000000..de3e513
--- /dev/null
+++ b/test/unit/firmware_service_test.mocks.dart
@@ -0,0 +1,658 @@
+// Mocks generated by Mockito 5.4.6 from annotations
+// in freegosy/test/unit/firmware_service_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i5;
+import 'dart:io' as _i6;
+import 'dart:typed_data' as _i7;
+
+import 'package:freegosy/core/emulator/emulator_strategy.dart' as _i10;
+import 'package:freegosy/core/emulator/strategy_registry.dart' as _i9;
+import 'package:freegosy/core/romm/romm_models.dart' as _i2;
+import 'package:freegosy/core/romm/romm_service.dart' as _i3;
+import 'package:freegosy/core/storage/directory_service.dart' as _i8;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:mockito/src/dummies.dart' as _i4;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: must_be_immutable
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+// ignore_for_file: invalid_use_of_internal_member
+
+class _FakeRomMConfig_0 extends _i1.SmartFake implements _i2.RomMConfig {
+  _FakeRomMConfig_0(Object parent, Invocation parentInvocation)
+    : super(parent, parentInvocation);
+}
+
+/// A class which mocks [RommService].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockRommService extends _i1.Mock implements _i3.RommService {
+  MockRommService() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.RomMConfig get config =>
+      (super.noSuchMethod(
+            Invocation.getter(#config),
+            returnValue: _FakeRomMConfig_0(this, Invocation.getter(#config)),
+          )
+          as _i2.RomMConfig);
+
+  @override
+  String get authHeader =>
+      (super.noSuchMethod(
+            Invocation.getter(#authHeader),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.getter(#authHeader),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<void> refreshToken() =>
+      (super.noSuchMethod(
+            Invocation.method(#refreshToken, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<List<_i2.Platform>> getPlatforms() =>
+      (super.noSuchMethod(
+            Invocation.method(#getPlatforms, []),
+            returnValue: _i5.Future<List<_i2.Platform>>.value(<_i2.Platform>[]),
+          )
+          as _i5.Future<List<_i2.Platform>>);
+
+  @override
+  _i5.Future<List<Map<String, dynamic>>> getCollections() =>
+      (super.noSuchMethod(
+            Invocation.method(#getCollections, []),
+            returnValue: _i5.Future<List<Map<String, dynamic>>>.value(
+              <Map<String, dynamic>>[],
+            ),
+          )
+          as _i5.Future<List<Map<String, dynamic>>>);
+
+  @override
+  String? resolveCoverUrl(_i2.Game? game) =>
+      (super.noSuchMethod(Invocation.method(#resolveCoverUrl, [game]))
+          as String?);
+
+  @override
+  _i5.Future<List<_i2.Game>> getGames(String? platformId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getGames, [platformId]),
+            returnValue: _i5.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+          )
+          as _i5.Future<List<_i2.Game>>);
+
+  @override
+  _i5.Future<List<_i2.Game>> getAllGames({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllGames, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+          )
+          as _i5.Future<List<_i2.Game>>);
+
+  @override
+  _i5.Future<List<_i2.Game>> getRecentlyPlayed({int? limit = 15}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getRecentlyPlayed, [], {#limit: limit}),
+            returnValue: _i5.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+          )
+          as _i5.Future<List<_i2.Game>>);
+
+  @override
+  _i5.Future<({List<_i2.Game> games, int total})> getGamesPage({
+    int? offset = 0,
+    int? limit = 50,
+    String? platformId,
+    String? search,
+    List<String>? genres = const [],
+    List<String>? regions = const [],
+    List<String>? languages = const [],
+    List<String>? collections = const [],
+    List<String>? statuses = const [],
+    bool? lastPlayed,
+    bool? withCharIndex = false,
+    bool? withFilterValues = false,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(#getGamesPage, [], {
+              #offset: offset,
+              #limit: limit,
+              #platformId: platformId,
+              #search: search,
+              #genres: genres,
+              #regions: regions,
+              #languages: languages,
+              #collections: collections,
+              #statuses: statuses,
+              #lastPlayed: lastPlayed,
+              #withCharIndex: withCharIndex,
+              #withFilterValues: withFilterValues,
+            }),
+            returnValue: _i5.Future<({List<_i2.Game> games, int total})>.value((
+              games: <_i2.Game>[],
+              total: 0,
+            )),
+          )
+          as _i5.Future<({List<_i2.Game> games, int total})>);
+
+  @override
+  _i5.Future<_i2.Game?> getRandomGame() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRandomGame, []),
+            returnValue: _i5.Future<_i2.Game?>.value(),
+          )
+          as _i5.Future<_i2.Game?>);
+
+  @override
+  _i5.Future<List<_i2.SaveFile>> getSaves(String? gameId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getSaves, [gameId]),
+            returnValue: _i5.Future<List<_i2.SaveFile>>.value(<_i2.SaveFile>[]),
+          )
+          as _i5.Future<List<_i2.SaveFile>>);
+
+  @override
+  String getDownloadUrl(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#getDownloadUrl, [game]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getDownloadUrl, [game]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<bool> uploadSave(
+    String? gameId,
+    _i6.File? saveFile, {
+    String? slot,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(#uploadSave, [gameId, saveFile], {#slot: slot}),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
+
+  @override
+  _i5.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #pruneOldSaves,
+              [gameId],
+              {#keepCount: keepCount},
+            ),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<List<Map<String, dynamic>>> getSavesList(String? gameId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getSavesList, [gameId]),
+            returnValue: _i5.Future<List<Map<String, dynamic>>>.value(
+              <Map<String, dynamic>>[],
+            ),
+          )
+          as _i5.Future<List<Map<String, dynamic>>>);
+
+  @override
+  _i5.Future<Map<String, dynamic>?> getLatestSave(String? gameId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getLatestSave, [gameId]),
+            returnValue: _i5.Future<Map<String, dynamic>?>.value(),
+          )
+          as _i5.Future<Map<String, dynamic>?>);
+
+  @override
+  _i5.Future<_i7.Uint8List?> downloadSave(String? saveUrl) =>
+      (super.noSuchMethod(
+            Invocation.method(#downloadSave, [saveUrl]),
+            returnValue: _i5.Future<_i7.Uint8List?>.value(),
+          )
+          as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<List<_i2.Firmware>> getFirmware({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmware, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Firmware>>.value(<_i2.Firmware>[]),
+          )
+          as _i5.Future<List<_i2.Firmware>>);
+
+  @override
+  String getFirmwareDownloadUrl(_i2.Firmware? firmware) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<_i7.Uint8List?> downloadFirmware(
+    _i2.Firmware? firmware, {
+    void Function(int, int)? onProgress,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #downloadFirmware,
+              [firmware],
+              {#onProgress: onProgress},
+            ),
+            returnValue: _i5.Future<_i7.Uint8List?>.value(),
+          )
+          as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<bool> updateRomProps(
+    String? romId, {
+    bool? backlogged,
+    bool? nowPlaying,
+    int? rating,
+    String? status,
+    int? completion,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #updateRomProps,
+              [romId],
+              {
+                #backlogged: backlogged,
+                #nowPlaying: nowPlaying,
+                #rating: rating,
+                #status: status,
+                #completion: completion,
+              },
+            ),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
+}
+
+/// A class which mocks [DirectoryService].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockDirectoryService extends _i1.Mock implements _i8.DirectoryService {
+  MockDirectoryService() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  String get romsRootPath =>
+      (super.noSuchMethod(
+            Invocation.getter(#romsRootPath),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.getter(#romsRootPath),
+            ),
+          )
+          as String);
+
+  @override
+  String get emulatorsRootPath =>
+      (super.noSuchMethod(
+            Invocation.getter(#emulatorsRootPath),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.getter(#emulatorsRootPath),
+            ),
+          )
+          as String);
+
+  @override
+  set romsRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#romsRootPath, value),
+    returnValueForMissingStub: null,
+  );
+
+  @override
+  set emulatorsRootPath(String? value) => super.noSuchMethod(
+    Invocation.setter(#emulatorsRootPath, value),
+    returnValueForMissingStub: null,
+  );
+
+  @override
+  _i5.Future<void> initialize() =>
+      (super.noSuchMethod(
+            Invocation.method(#initialize, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<void> loadEmulatorPathOverrides() =>
+      (super.noSuchMethod(
+            Invocation.method(#loadEmulatorPathOverrides, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<void> setEmulatorPathOverride(String? emulatorId, String? path) =>
+      (super.noSuchMethod(
+            Invocation.method(#setEmulatorPathOverride, [emulatorId, path]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  String? getEmulatorPathOverride(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorPathOverride, [emulatorId]),
+          )
+          as String?);
+
+  @override
+  _i5.Future<void> setRomsRoot(String? path) =>
+      (super.noSuchMethod(
+            Invocation.method(#setRomsRoot, [path]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<void> setEmulatorsRoot(String? path) =>
+      (super.noSuchMethod(
+            Invocation.method(#setEmulatorsRoot, [path]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<String> getRomsDirectory() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRomsDirectory, []),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getRomsDirectory, []),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<Set<String>> getAllDownloadedFileNames() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNames, []),
+            returnValue: _i5.Future<Set<String>>.value(<String>{}),
+          )
+          as _i5.Future<Set<String>>);
+
+  @override
+  _i5.Future<Map<String, Set<String>>> getAllDownloadedFileNamesByPlatform() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNamesByPlatform, []),
+            returnValue: _i5.Future<Map<String, Set<String>>>.value(
+              <String, Set<String>>{},
+            ),
+          )
+          as _i5.Future<Map<String, Set<String>>>);
+
+  @override
+  _i5.Future<String> getRomDirectory(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#getRomDirectory, [game]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getRomDirectory, [game]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String> getRomFilePath(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#getRomFilePath, [game]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getRomFilePath, [game]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String?> findExistingRomPath(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#findExistingRomPath, [game]),
+            returnValue: _i5.Future<String?>.value(),
+          )
+          as _i5.Future<String?>);
+
+  @override
+  _i5.Future<String?> resolveSevenZipPath() =>
+      (super.noSuchMethod(
+            Invocation.method(#resolveSevenZipPath, []),
+            returnValue: _i5.Future<String?>.value(),
+          )
+          as _i5.Future<String?>);
+
+  @override
+  _i5.Future<String> getEmulatorDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String> getEmulatorAppSupportDirectory(String? emulatorName) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorAppSupportDirectory, [emulatorName]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorAppSupportDirectory, [
+                  emulatorName,
+                ]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String> getEmulatorBiosDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String> getEmulatorSystemDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorSystemDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorSystemDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<void> deleteEmulator(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#deleteEmulator, [emulatorId]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<String> getEmulatorExecutable(
+    String? emulatorId,
+    String? executableName,
+  ) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorExecutable, [
+              emulatorId,
+              executableName,
+            ]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorExecutable, [
+                  emulatorId,
+                  executableName,
+                ]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
+  @override
+  _i5.Future<String?> findEmulatorExecutable(
+    String? emulatorId,
+    String? executableName,
+  ) =>
+      (super.noSuchMethod(
+            Invocation.method(#findEmulatorExecutable, [
+              emulatorId,
+              executableName,
+            ]),
+            returnValue: _i5.Future<String?>.value(),
+          )
+          as _i5.Future<String?>);
+
+  @override
+  _i5.Future<bool> isEmulatorInstalled(
+    String? emulatorId,
+    String? executableName,
+  ) =>
+      (super.noSuchMethod(
+            Invocation.method(#isEmulatorInstalled, [
+              emulatorId,
+              executableName,
+            ]),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
+
+  @override
+  _i5.Future<bool> isRomDownloaded(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#isRomDownloaded, [game]),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
+
+  @override
+  _i5.Future<void> deleteRom(_i2.Game? game) =>
+      (super.noSuchMethod(
+            Invocation.method(#deleteRom, [game]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+}
+
+/// A class which mocks [StrategyRegistry].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockStrategyRegistry extends _i1.Mock implements _i9.StrategyRegistry {
+  MockStrategyRegistry() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  Map<String, List<_i10.EmulatorStrategy>> detectConflicts() =>
+      (super.noSuchMethod(
+            Invocation.method(#detectConflicts, []),
+            returnValue: <String, List<_i10.EmulatorStrategy>>{},
+          )
+          as Map<String, List<_i10.EmulatorStrategy>>);
+
+  @override
+  String? getPreferredEmulatorId(String? slug) =>
+      (super.noSuchMethod(Invocation.method(#getPreferredEmulatorId, [slug]))
+          as String?);
+
+  @override
+  _i5.Future<void> loadPreferences() =>
+      (super.noSuchMethod(
+            Invocation.method(#loadPreferences, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<void> setPreference(String? canonicalSlug, String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#setPreference, [canonicalSlug, emulatorId]),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i5.Future<void> clearPreferences() =>
+      (super.noSuchMethod(
+            Invocation.method(#clearPreferences, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
+  @override
+  _i10.EmulatorStrategy? getStrategyForSlug(String? platformSlug) =>
+      (super.noSuchMethod(
+            Invocation.method(#getStrategyForSlug, [platformSlug]),
+          )
+          as _i10.EmulatorStrategy?);
+
+  @override
+  _i10.EmulatorStrategy? getStrategyById(String? id) =>
+      (super.noSuchMethod(Invocation.method(#getStrategyById, [id]))
+          as _i10.EmulatorStrategy?);
+
+  @override
+  Map<String, dynamic>? getDefinition(String? emulatorId) =>
+      (super.noSuchMethod(Invocation.method(#getDefinition, [emulatorId]))
+          as Map<String, dynamic>?);
+}
diff --git a/test/unit/paginated_games_provider_test.mocks.dart b/test/unit/paginated_games_provider_test.mocks.dart
index 21473d2..bd6bbd8 100644
--- a/test/unit/paginated_games_provider_test.mocks.dart
+++ b/test/unit/paginated_games_provider_test.mocks.dart
@@ -59,6 +59,15 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as String);
 
+  @override
+  _i5.Future<void> refreshToken() =>
+      (super.noSuchMethod(
+            Invocation.method(#refreshToken, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
   @override
   _i5.Future<List<_i2.Platform>> getPlatforms() =>
       (super.noSuchMethod(
@@ -143,6 +152,14 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as _i5.Future<({List<_i2.Game> games, int total})>);
 
+  @override
+  _i5.Future<_i2.Game?> getRandomGame() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRandomGame, []),
+            returnValue: _i5.Future<_i2.Game?>.value(),
+          )
+          as _i5.Future<_i2.Game?>);
+
   @override
   _i5.Future<List<_i2.SaveFile>> getSaves(String? gameId) =>
       (super.noSuchMethod(
@@ -212,4 +229,63 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
             returnValue: _i5.Future<_i7.Uint8List?>.value(),
           )
           as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<List<_i2.Firmware>> getFirmware({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmware, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Firmware>>.value(<_i2.Firmware>[]),
+          )
+          as _i5.Future<List<_i2.Firmware>>);
+
+  @override
+  String getFirmwareDownloadUrl(_i2.Firmware? firmware) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<_i7.Uint8List?> downloadFirmware(
+    _i2.Firmware? firmware, {
+    void Function(int, int)? onProgress,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #downloadFirmware,
+              [firmware],
+              {#onProgress: onProgress},
+            ),
+            returnValue: _i5.Future<_i7.Uint8List?>.value(),
+          )
+          as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<bool> updateRomProps(
+    String? romId, {
+    bool? backlogged,
+    bool? nowPlaying,
+    int? rating,
+    String? status,
+    int? completion,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #updateRomProps,
+              [romId],
+              {
+                #backlogged: backlogged,
+                #nowPlaying: nowPlaying,
+                #rating: rating,
+                #status: status,
+                #completion: completion,
+              },
+            ),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
 }
diff --git a/test/unit/save_sync_service_test.mocks.dart b/test/unit/save_sync_service_test.mocks.dart
index c5a9278..dd66357 100644
--- a/test/unit/save_sync_service_test.mocks.dart
+++ b/test/unit/save_sync_service_test.mocks.dart
@@ -62,6 +62,15 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as String);
 
+  @override
+  _i5.Future<void> refreshToken() =>
+      (super.noSuchMethod(
+            Invocation.method(#refreshToken, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
   @override
   _i5.Future<List<_i2.Platform>> getPlatforms() =>
       (super.noSuchMethod(
@@ -146,6 +155,14 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as _i5.Future<({List<_i2.Game> games, int total})>);
 
+  @override
+  _i5.Future<_i2.Game?> getRandomGame() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRandomGame, []),
+            returnValue: _i5.Future<_i2.Game?>.value(),
+          )
+          as _i5.Future<_i2.Game?>);
+
   @override
   _i5.Future<List<_i2.SaveFile>> getSaves(String? gameId) =>
       (super.noSuchMethod(
@@ -215,6 +232,65 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
             returnValue: _i5.Future<_i7.Uint8List?>.value(),
           )
           as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<List<_i2.Firmware>> getFirmware({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmware, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Firmware>>.value(<_i2.Firmware>[]),
+          )
+          as _i5.Future<List<_i2.Firmware>>);
+
+  @override
+  String getFirmwareDownloadUrl(_i2.Firmware? firmware) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<_i7.Uint8List?> downloadFirmware(
+    _i2.Firmware? firmware, {
+    void Function(int, int)? onProgress,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #downloadFirmware,
+              [firmware],
+              {#onProgress: onProgress},
+            ),
+            returnValue: _i5.Future<_i7.Uint8List?>.value(),
+          )
+          as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<bool> updateRomProps(
+    String? romId, {
+    bool? backlogged,
+    bool? nowPlaying,
+    int? rating,
+    String? status,
+    int? completion,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #updateRomProps,
+              [romId],
+              {
+                #backlogged: backlogged,
+                #nowPlaying: nowPlaying,
+                #rating: rating,
+                #status: status,
+                #completion: completion,
+              },
+            ),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
 }
 
 /// A class which mocks [DirectoryService].
@@ -332,6 +408,16 @@ class MockDirectoryService extends _i1.Mock implements _i8.DirectoryService {
           )
           as _i5.Future<Set<String>>);
 
+  @override
+  _i5.Future<Map<String, Set<String>>> getAllDownloadedFileNamesByPlatform() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNamesByPlatform, []),
+            returnValue: _i5.Future<Map<String, Set<String>>>.value(
+              <String, Set<String>>{},
+            ),
+          )
+          as _i5.Future<Map<String, Set<String>>>);
+
   @override
   _i5.Future<String> getRomDirectory(_i2.Game? game) =>
       (super.noSuchMethod(
@@ -402,6 +488,19 @@ class MockDirectoryService extends _i1.Mock implements _i8.DirectoryService {
           )
           as _i5.Future<String>);
 
+  @override
+  _i5.Future<String> getEmulatorBiosDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
   @override
   _i5.Future<String> getEmulatorSystemDirectory(String? emulatorId) =>
       (super.noSuchMethod(
diff --git a/test/unit/strategy_registry_test.mocks.dart b/test/unit/strategy_registry_test.mocks.dart
index 4d715a3..e7fd644 100644
--- a/test/unit/strategy_registry_test.mocks.dart
+++ b/test/unit/strategy_registry_test.mocks.dart
@@ -140,6 +140,16 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
           )
           as _i4.Future<Set<String>>);
 
+  @override
+  _i4.Future<Map<String, Set<String>>> getAllDownloadedFileNamesByPlatform() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNamesByPlatform, []),
+            returnValue: _i4.Future<Map<String, Set<String>>>.value(
+              <String, Set<String>>{},
+            ),
+          )
+          as _i4.Future<Map<String, Set<String>>>);
+
   @override
   _i4.Future<String> getRomDirectory(_i5.Game? game) =>
       (super.noSuchMethod(
@@ -210,6 +220,19 @@ class MockDirectoryService extends _i1.Mock implements _i2.DirectoryService {
           )
           as _i4.Future<String>);
 
+  @override
+  _i4.Future<String> getEmulatorBiosDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+            returnValue: _i4.Future<String>.value(
+              _i3.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i4.Future<String>);
+
   @override
   _i4.Future<String> getEmulatorSystemDirectory(String? emulatorId) =>
       (super.noSuchMethod(
diff --git a/test/widgets/library_screen_test.mocks.dart b/test/widgets/library_screen_test.mocks.dart
index 417891d..44ab193 100644
--- a/test/widgets/library_screen_test.mocks.dart
+++ b/test/widgets/library_screen_test.mocks.dart
@@ -60,6 +60,15 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as String);
 
+  @override
+  _i5.Future<void> refreshToken() =>
+      (super.noSuchMethod(
+            Invocation.method(#refreshToken, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
   @override
   _i5.Future<List<_i2.Platform>> getPlatforms() =>
       (super.noSuchMethod(
@@ -144,6 +153,14 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
           )
           as _i5.Future<({List<_i2.Game> games, int total})>);
 
+  @override
+  _i5.Future<_i2.Game?> getRandomGame() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRandomGame, []),
+            returnValue: _i5.Future<_i2.Game?>.value(),
+          )
+          as _i5.Future<_i2.Game?>);
+
   @override
   _i5.Future<List<_i2.SaveFile>> getSaves(String? gameId) =>
       (super.noSuchMethod(
@@ -213,6 +230,65 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
             returnValue: _i5.Future<_i7.Uint8List?>.value(),
           )
           as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<List<_i2.Firmware>> getFirmware({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmware, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Firmware>>.value(<_i2.Firmware>[]),
+          )
+          as _i5.Future<List<_i2.Firmware>>);
+
+  @override
+  String getFirmwareDownloadUrl(_i2.Firmware? firmware) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<_i7.Uint8List?> downloadFirmware(
+    _i2.Firmware? firmware, {
+    void Function(int, int)? onProgress,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #downloadFirmware,
+              [firmware],
+              {#onProgress: onProgress},
+            ),
+            returnValue: _i5.Future<_i7.Uint8List?>.value(),
+          )
+          as _i5.Future<_i7.Uint8List?>);
+
+  @override
+  _i5.Future<bool> updateRomProps(
+    String? romId, {
+    bool? backlogged,
+    bool? nowPlaying,
+    int? rating,
+    String? status,
+    int? completion,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #updateRomProps,
+              [romId],
+              {
+                #backlogged: backlogged,
+                #nowPlaying: nowPlaying,
+                #rating: rating,
+                #status: status,
+                #completion: completion,
+              },
+            ),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
 }
 
 /// A class which mocks [DirectoryService].
@@ -330,6 +406,16 @@ class MockDirectoryService extends _i1.Mock implements _i8.DirectoryService {
           )
           as _i5.Future<Set<String>>);
 
+  @override
+  _i5.Future<Map<String, Set<String>>> getAllDownloadedFileNamesByPlatform() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNamesByPlatform, []),
+            returnValue: _i5.Future<Map<String, Set<String>>>.value(
+              <String, Set<String>>{},
+            ),
+          )
+          as _i5.Future<Map<String, Set<String>>>);
+
   @override
   _i5.Future<String> getRomDirectory(_i2.Game? game) =>
       (super.noSuchMethod(
@@ -400,6 +486,19 @@ class MockDirectoryService extends _i1.Mock implements _i8.DirectoryService {
           )
           as _i5.Future<String>);
 
+  @override
+  _i5.Future<String> getEmulatorBiosDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
   @override
   _i5.Future<String> getEmulatorSystemDirectory(String? emulatorId) =>
       (super.noSuchMethod(
diff --git a/test/widgets/settings_screen_test.mocks.dart b/test/widgets/settings_screen_test.mocks.dart
index 26dc679..ad21c9e 100644
--- a/test/widgets/settings_screen_test.mocks.dart
+++ b/test/widgets/settings_screen_test.mocks.dart
@@ -150,6 +150,16 @@ class MockDirectoryService extends _i1.Mock implements _i3.DirectoryService {
           )
           as _i5.Future<Set<String>>);
 
+  @override
+  _i5.Future<Map<String, Set<String>>> getAllDownloadedFileNamesByPlatform() =>
+      (super.noSuchMethod(
+            Invocation.method(#getAllDownloadedFileNamesByPlatform, []),
+            returnValue: _i5.Future<Map<String, Set<String>>>.value(
+              <String, Set<String>>{},
+            ),
+          )
+          as _i5.Future<Map<String, Set<String>>>);
+
   @override
   _i5.Future<String> getRomDirectory(_i2.Game? game) =>
       (super.noSuchMethod(
@@ -220,6 +230,19 @@ class MockDirectoryService extends _i1.Mock implements _i3.DirectoryService {
           )
           as _i5.Future<String>);
 
+  @override
+  _i5.Future<String> getEmulatorBiosDirectory(String? emulatorId) =>
+      (super.noSuchMethod(
+            Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+            returnValue: _i5.Future<String>.value(
+              _i4.dummyValue<String>(
+                this,
+                Invocation.method(#getEmulatorBiosDirectory, [emulatorId]),
+              ),
+            ),
+          )
+          as _i5.Future<String>);
+
   @override
   _i5.Future<String> getEmulatorSystemDirectory(String? emulatorId) =>
       (super.noSuchMethod(
@@ -337,6 +360,15 @@ class MockRommService extends _i1.Mock implements _i6.RommService {
           )
           as String);
 
+  @override
+  _i5.Future<void> refreshToken() =>
+      (super.noSuchMethod(
+            Invocation.method(#refreshToken, []),
+            returnValue: _i5.Future<void>.value(),
+            returnValueForMissingStub: _i5.Future<void>.value(),
+          )
+          as _i5.Future<void>);
+
   @override
   _i5.Future<List<_i2.Platform>> getPlatforms() =>
       (super.noSuchMethod(
@@ -421,6 +453,14 @@ class MockRommService extends _i1.Mock implements _i6.RommService {
           )
           as _i5.Future<({List<_i2.Game> games, int total})>);
 
+  @override
+  _i5.Future<_i2.Game?> getRandomGame() =>
+      (super.noSuchMethod(
+            Invocation.method(#getRandomGame, []),
+            returnValue: _i5.Future<_i2.Game?>.value(),
+          )
+          as _i5.Future<_i2.Game?>);
+
   @override
   _i5.Future<List<_i2.SaveFile>> getSaves(String? gameId) =>
       (super.noSuchMethod(
@@ -490,6 +530,65 @@ class MockRommService extends _i1.Mock implements _i6.RommService {
             returnValue: _i5.Future<_i8.Uint8List?>.value(),
           )
           as _i5.Future<_i8.Uint8List?>);
+
+  @override
+  _i5.Future<List<_i2.Firmware>> getFirmware({String? platformId}) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmware, [], {#platformId: platformId}),
+            returnValue: _i5.Future<List<_i2.Firmware>>.value(<_i2.Firmware>[]),
+          )
+          as _i5.Future<List<_i2.Firmware>>);
+
+  @override
+  String getFirmwareDownloadUrl(_i2.Firmware? firmware) =>
+      (super.noSuchMethod(
+            Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            returnValue: _i4.dummyValue<String>(
+              this,
+              Invocation.method(#getFirmwareDownloadUrl, [firmware]),
+            ),
+          )
+          as String);
+
+  @override
+  _i5.Future<_i8.Uint8List?> downloadFirmware(
+    _i2.Firmware? firmware, {
+    void Function(int, int)? onProgress,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #downloadFirmware,
+              [firmware],
+              {#onProgress: onProgress},
+            ),
+            returnValue: _i5.Future<_i8.Uint8List?>.value(),
+          )
+          as _i5.Future<_i8.Uint8List?>);
+
+  @override
+  _i5.Future<bool> updateRomProps(
+    String? romId, {
+    bool? backlogged,
+    bool? nowPlaying,
+    int? rating,
+    String? status,
+    int? completion,
+  }) =>
+      (super.noSuchMethod(
+            Invocation.method(
+              #updateRomProps,
+              [romId],
+              {
+                #backlogged: backlogged,
+                #nowPlaying: nowPlaying,
+                #rating: rating,
+                #status: status,
+                #completion: completion,
+              },
+            ),
+            returnValue: _i5.Future<bool>.value(false),
+          )
+          as _i5.Future<bool>);
 }
 
 /// A class which mocks [StrategyRegistry].

Clone this wiki locally