-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit d973766
abduznik edited this page May 23, 2026
·
1 revision
Commit: d973766f288e1eac4e11c3bcfd19202fbe30e57c
Author: abduznik
Date: 2026-04-18
Why: Adds a new feature or capability to the application.
lib/core/romm/rom_scanner_service.dart | 142 ++++++++++++++++++++
lib/core/romm/romm_service.dart | 25 ++++
lib/core/storage/rom_mapping_service.dart | 62 +++++++++
lib/main.dart | 4 +-
lib/providers/downloaded_games_cache_provider.dart | 143 ++++++---------------
lib/providers/romm_provider.dart | 18 +++
lib/ui/screens/library_screen.dart | 4 +-
pubspec.lock | 16 +++
pubspec.yaml | 2 +
test/unit/firmware_service_test.mocks.dart | 24 ++++
test/unit/paginated_games_provider_test.mocks.dart | 24 ++++
test/unit/save_sync_service_test.mocks.dart | 24 ++++
test/widgets/library_screen_test.dart | 16 ++-
test/widgets/library_screen_test.mocks.dart | 109 ++++++++++++++++
test/widgets/settings_screen_test.mocks.dart | 24 ++++
15 files changed, 529 insertions(+), 108 deletions(-)
lib/core/romm/rom_scanner_service.dartlib/core/romm/romm_service.dartlib/core/storage/rom_mapping_service.dartlib/main.dartlib/providers/downloaded_games_cache_provider.dartlib/providers/romm_provider.dartlib/ui/screens/library_screen.dartpubspec.lockpubspec.yamltest/unit/firmware_service_test.mocks.darttest/unit/paginated_games_provider_test.mocks.darttest/unit/save_sync_service_test.mocks.darttest/widgets/library_screen_test.darttest/widgets/library_screen_test.mocks.darttest/widgets/settings_screen_test.mocks.dart
diff --git a/lib/core/romm/rom_scanner_service.dart b/lib/core/romm/rom_scanner_service.dart
new file mode 100644
index 0000000..cb84801
--- /dev/null
+++ b/lib/core/romm/rom_scanner_service.dart
@@ -0,0 +1,142 @@
+import 'dart:io';
+import 'dart:isolate';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as p;
+import 'romm_service.dart';
+import '../storage/rom_mapping_service.dart';
+import 'romm_models.dart';
+import 'package:crypto/crypto.dart';
+
+class RomSyncResult {
+ final String path;
+ final String? romId;
+ final Game? game;
+
+ RomSyncResult(this.path, this.romId, {this.game});
+}
+
+class RomScannerService {
+ final RommService _rommService;
+ final RomMappingService _mappingService;
+
+ RomScannerService(this._rommService, this._mappingService);
+
+ /// Performs an incremental sync of the ROM directory.
+ Stream<RomSyncResult> sync(String romsRoot) async* {
+ final storedMTimes = _mappingService.getMTimes();
+ final mappings = _mappingService.getMappings();
+
+ final rootDir = Directory(romsRoot);
+ if (!await rootDir.exists()) return;
+
+ // Phase 1: Check root and platform directories for changes
+ final rootStat = await rootDir.stat();
+ bool rootChanged = storedMTimes[romsRoot] != rootStat.modified.millisecondsSinceEpoch;
+
+ final List<Directory> dirtyDirs = [];
+ if (rootChanged) {
+ dirtyDirs.add(rootDir);
+ }
+
+ // Even if root mtime hasn't changed, we should check platform subdirs
+ // because some filesystems don't propagate mtime changes upwards.
+ await for (final entity in rootDir.list()) {
+ if (entity is Directory) {
+ final platformMTime = storedMTimes[entity.path];
+ final stat = await entity.stat();
+ if (platformMTime != stat.modified.millisecondsSinceEpoch) {
+ dirtyDirs.add(entity);
+ }
+ }
+ }
+
+ if (dirtyDirs.isEmpty) {
+ debugPrint('[RomScanner] No directories changed. Skipping scan.');
+ return;
+ }
+
+ debugPrint('[RomScanner] Scanning ${dirtyDirs.length} dirty directories...');
+
+ // Phase 2: Identify new/removed files in dirty directories using an Isolate
+ final List<String> dirPaths = dirtyDirs.map((d) => d.path).toList();
+ final List<String> allFiles = await Isolate.run(() => _scanDirectories(dirPaths));
+
+ // Phase 3: Update mappings and match new files
+ final Map<String, int> newMTimes = Map.from(storedMTimes);
+ for (final dir in dirtyDirs) {
+ final stat = await dir.stat();
+ newMTimes[dir.path] = stat.modified.millisecondsSinceEpoch;
+ }
+ await _mappingService.saveMTimes(newMTimes);
+
+ final Set<String> existingFiles = mappings.keys.toSet();
+ final Set<String> currentFilesSet = allFiles.toSet();
+
+ // Identify truly new files
+ final List<String> newFiles = allFiles.where((f) => !existingFiles.contains(f)).toList();
+
+ // Clean up removed files from mappings
+ final List<String> removedFiles = existingFiles.where((f) => !currentFilesSet.contains(f)).toList();
+ if (removedFiles.isNotEmpty) {
+ final updatedMappings = Map<String, String>.from(mappings);
+ for (final f in removedFiles) {
+ updatedMappings.remove(f);
+ }
+ await _mappingService.saveMappings(updatedMappings);
+ }
+
+ if (newFiles.isEmpty) {
+ debugPrint('[RomScanner] No new files found in dirty directories.');
+ return;
+ }
+
+ debugPrint('[RomScanner] Matching ${newFiles.length} new files...');
+
+ // Phase 4: Match new files via RomM API
+ for (final filePath in newFiles) {
+ final fileName = p.basename(filePath);
+
+ // Try filename match first (fast)
+ final searchResult = await _rommService.searchRoms(search: fileName);
+ if (searchResult.isNotEmpty) {
+ final game = searchResult.first;
+ await _mappingService.updateMapping(filePath, game.id);
+ yield RomSyncResult(filePath, game.id, game: game);
+ continue;
+ }
+
+ // Optional: SHA1 match (slower, could be triggered by user or done here)
+ // For now, let's keep it simple and just yield the path if not found
+ yield RomSyncResult(filePath, null);
+ }
+ }
+
+ /// Calculates SHA1 for a file. Can be used for deep matching.
+ static Future<String> calculateSha1(String path) async {
+ final file = File(path);
+ if (!await file.exists()) return '';
+ final bytes = await file.readAsBytes();
+ return sha1.convert(bytes).toString();
+ }
+}
+
+/// Helper function to be run in an Isolate
+List<String> _scanDirectories(List<String> paths) {
+ final List<String> files = [];
+ for (final path in paths) {
+ final dir = Directory(path);
+ if (dir.existsSync()) {
+ // Shallow scan: only files in the directory
+ // (Assuming ROMs/Platform/File structure)
+ for (final entity in dir.listSync()) {
+ if (entity is File) {
+ files.add(entity.path);
+ } else if (entity is Directory) {
+ // Handle folders that are treated as games (e.g. Windows)
+ files.add(entity.path);
+ }
+ }
+ }
+ }
+ return files;
+}
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index 0895025..34baf4b 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -201,6 +201,31 @@ class RommService {
} catch (_) { return []; }
}
+ Future<List<Game>> searchRoms({String? sha1, String? md5, String? search}) async {
+ final params = <String, dynamic>{
+ 'limit': 10,
+ 'offset': 0,
+ 'with_char_index': false,
+ 'with_filter_values': false,
+ };
+ if (sha1 != null) params['sha1'] = sha1;
+ if (md5 != null) params['md5'] = md5;
+ if (search != null) params['search_term'] = search;
+
+ try {
+ final response = await _dio.get('/api/roms', queryParameters: params, options: _authOptions);
+ if (response.statusCode == 200) {
+ final Map<String, dynamic> data = response.data is Map ? response.data : {'items': response.data};
+ final List<dynamic> items = data['items'] ?? [];
+ return items.map((e) => Game.fromJson(e as Map<String, dynamic>)).toList();
+ }
+ return [];
+ } catch (e) {
+ debugPrint('[RomM] searchRoms error: $e');
+ return [];
+ }
+ }
+
Future<({List<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}) async {
final params = <String, dynamic>{'limit': limit, 'offset': offset, 'order_by': 'name', 'order_dir': 'asc', 'with_char_index': withCharIndex, 'with_filter_values': withFilterValues};
if (lastPlayed != null) params['last_played'] = lastPlayed;
diff --git a/lib/core/storage/rom_mapping_service.dart b/lib/core/storage/rom_mapping_service.dart
new file mode 100644
index 0000000..6c9dcfc
--- /dev/null
+++ b/lib/core/storage/rom_mapping_service.dart
@@ -0,0 +1,62 @@
+import 'package:hive_flutter/hive_flutter.dart';
+
+class RomMappingService {
+ static const String _boxName = 'rom_mappings_v2';
+ late Box _box;
+
+ static const String _keyMappings = 'path_to_id';
+ static const String _keyMTimes = 'dir_mtimes';
+ static const String _keyLastSync = 'last_sync_time';
+
+ Future<void> init() async {
+ _box = await Hive.openBox(_boxName);
+ }
+
+ /// Map of FilePath -> RomID
+ Map<String, String> getMappings() {
+ final data = _box.get(_keyMappings);
+ if (data is Map) {
+ return Map<String, String>.from(data);
+ }
+ return {};
+ }
+
+ Future<void> saveMappings(Map<String, String> mappings) async {
+ await _box.put(_keyMappings, mappings);
+ }
+
+ Future<void> updateMapping(String path, String romId) async {
+ final mappings = getMappings();
+ mappings[path] = romId;
+ await saveMappings(mappings);
+ }
+
+ /// Map of DirectoryPath -> LastModifiedTimestamp (ms)
+ Map<String, int> getMTimes() {
+ final data = _box.get(_keyMTimes);
+ if (data is Map) {
+ return Map<String, int>.from(data);
+ }
+ return {};
+ }
+
+ Future<void> saveMTimes(Map<String, int> mtimes) async {
+ await _box.put(_keyMTimes, mtimes);
+ }
+
+ int? getLastSyncTime() {
+ return _box.get(_keyLastSync) as int?;
+ }
+
+ Future<void> setLastSyncTime(int timestamp) async {
+ await _box.put(_keyLastSync, timestamp);
+ }
+
+ String? getRomIdForPath(String path) {
+ return getMappings()[path];
+ }
+
+ Future<void> clear() async {
+ await _box.clear();
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 4f4a952..d1910f7 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:hive_flutter/hive_flutter.dart';
import 'app.dart';
-void main() {
+void main() async {
WidgetsFlutterBinding.ensureInitialized();
+ await Hive.initFlutter();
runApp(
const ProviderScope(
child: FreegosyApp(),
diff --git a/lib/providers/downloaded_games_cache_provider.dart b/lib/providers/downloaded_games_cache_provider.dart
index de824bd..ff64974 100644
--- a/lib/providers/downloaded_games_cache_provider.dart
+++ b/lib/providers/downloaded_games_cache_provider.dart
@@ -1,5 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../core/romm/romm_models.dart';
import 'download_provider.dart';
import 'romm_provider.dart';
import 'dart:async';
@@ -7,18 +6,18 @@ import 'package:flutter/foundation.dart';
class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
final Ref _ref;
- bool _isDeepScanning = false;
- Timer? _scanTimer;
+ bool _isSyncing = false;
+ Timer? _syncTimer;
Timer? _periodicTimer;
DownloadedGamesCache(this._ref) : super({}) {
_init();
}
- bool get isDeepScanning => _isDeepScanning;
+ bool get isSyncing => _isSyncing;
void _init() {
- // Initial scan of whatever is already in memory
+ // Initial load from local mappings
refresh();
// Listen to download progress
@@ -38,136 +37,72 @@ class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
}
});
- // Start deep scan in background after a short delay
- _scanTimer = Timer(const Duration(seconds: 5), () => startDeepScan());
+ // Start incremental sync in background after a short delay
+ _syncTimer = Timer(const Duration(seconds: 3), () => startIncrementalSync());
- // Periodical refresh every 2 minutes to catch external filesystem changes
- _periodicTimer = Timer.periodic(const Duration(minutes: 2), (_) => refresh());
+ // Periodical refresh every 5 minutes
+ _periodicTimer = Timer.periodic(const Duration(minutes: 5), (_) => startIncrementalSync());
}
@override
void dispose() {
- _scanTimer?.cancel();
+ _syncTimer?.cancel();
_periodicTimer?.cancel();
- _isDeepScanning = false;
+ _isSyncing = false;
super.dispose();
}
- /// Quickly refreshes the download status for games currently in the metadata cache.
+ /// Quickly populates the cache from locally stored mappings.
Future<void> refresh() async {
- final directoryService = _ref.read(directoryServiceProvider).asData?.value;
- if (directoryService == null) return;
+ final mappingService = _ref.read(romMappingServiceProvider).asData?.value;
+ if (mappingService == null) return;
- final downloadedByPlatform = await directoryService.getAllDownloadedFileNamesByPlatform();
- final metadataCache = _ref.read(metadataCacheServiceProvider).asData?.value;
-
- final List<Game> gamesToScan = metadataCache?.cachedGames ?? [];
- if (gamesToScan.isEmpty) return;
-
- final Map<String, bool> newState = Map<String, bool>.from(state);
- for (final game in gamesToScan) {
- final platformSlug = game.platformSlug ?? '';
- final downloadedNames = downloadedByPlatform[platformSlug] ?? {};
-
- final fileName = (game.fsName ?? game.fileName ?? game.name)
- .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
- .toLowerCase();
-
- newState[game.id] = downloadedNames.contains(fileName);
+ final mappings = mappingService.getMappings();
+ final Map<String, bool> newState = {};
+ for (final romId in mappings.values) {
+ newState[romId] = true;
}
state = newState;
}
- /// Aggressively fetches all games from RomM in the background to find EVERYTHING downloaded.
- Future<void> startDeepScan() async {
- if (_isDeepScanning) return;
+ /// Runs the high-performance incremental sync.
+ Future<void> startIncrementalSync() async {
+ if (_isSyncing) return;
- final service = _ref.read(rommServiceProvider);
+ final scanner = _ref.read(romScannerServiceProvider);
final dirService = _ref.read(directoryServiceProvider).asData?.value;
final metadataCache = _ref.read(metadataCacheServiceProvider).asData?.value;
- if (service == null || dirService == null || metadataCache == null) {
+ if (scanner == null || dirService == null || metadataCache == null) {
// Retry in a bit if services aren't ready
- Future.delayed(const Duration(seconds: 10), () => startDeepScan());
+ Future.delayed(const Duration(seconds: 10), () => startIncrementalSync());
return;
}
- _isDeepScanning = true;
- debugPrint('[DownloadedGamesCache] Starting background deep scan...');
+ _isSyncing = true;
+ debugPrint('[DownloadedGamesCache] Starting incremental sync...');
try {
- final platforms = await service.getPlatforms();
- final downloadedByPlatform = await dirService.getAllDownloadedFileNamesByPlatform();
+ final romsRoot = await dirService.getRomsDirectory();
- if (downloadedByPlatform.isEmpty) {
- _isDeepScanning = false;
- return;
- }
-
- final Map<String, bool> newState = Map<String, bool>.from(state);
-
- for (final platform in platforms) {
- final slug = platform.slug;
- if (!downloadedByPlatform.containsKey(slug) && !downloadedByPlatform.containsKey(platform.name.toLowerCase())) {
- continue;
- }
-
- final localFiles = downloadedByPlatform[slug] ?? downloadedByPlatform[platform.name.toLowerCase()] ?? {};
- if (localFiles.isEmpty) continue;
-
- debugPrint('[DownloadedGamesCache] Scanning platform ${platform.name} for ${localFiles.length} local files...');
-
- int offset = 0;
- const int limit = 100;
-
- while (true) {
- final result = await service.getGamesPage(
- platformId: platform.id.toString(),
- offset: offset,
- limit: limit,
- );
-
- if (result.games.isEmpty) break;
-
- // Save to metadata cache so they are available offline
- await metadataCache.saveGames(result.games);
-
- for (final game in result.games) {
- final fileName = (game.fsName ?? game.fileName ?? game.name)
- .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
- .toLowerCase();
-
- if (localFiles.contains(fileName)) {
- newState[game.id] = true;
- } else {
- // Also check without extension if local file might have different extension
- final stem = fileName.contains('.') ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName;
- bool found = false;
- for (final localFile in localFiles) {
- if (localFile.startsWith(stem)) {
- found = true;
- break;
- }
- }
- if (found) newState[game.id] = true;
- }
+ await for (final result in scanner.sync(romsRoot)) {
+ if (result.romId != null) {
+ // Found a match!
+ final newState = Map<String, bool>.from(state);
+ newState[result.romId!] = true;
+ state = newState;
+
+ // If we have the game object, cache it for offline use
+ if (result.game != null) {
+ await metadataCache.saveGames([result.game!]);
}
-
- // Update state incrementally so UI sees progress
- state = Map<String, bool>.from(newState);
-
- offset += limit;
- if (offset >= result.total) break;
-
- // Small gap to not overwhelm the server
- await Future.delayed(const Duration(milliseconds: 500));
}
}
} catch (e) {
- debugPrint('[DownloadedGamesCache] Deep scan error: $e');
+ debugPrint('[DownloadedGamesCache] Sync error: $e');
} finally {
- _isDeepScanning = false;
- debugPrint('[DownloadedGamesCache] Background deep scan complete.');
+ _isSyncing = false;
+ debugPrint('[DownloadedGamesCache] Incremental sync complete.');
}
}
diff --git a/lib/providers/romm_provider.dart b/lib/providers/romm_provider.dart
index 803aa5c..f26fbdb 100644
--- a/lib/providers/romm_provider.dart
+++ b/lib/providers/romm_provider.dart
@@ -11,6 +11,8 @@ import 'package:freegosy/core/emulator/emulator_registry_data.dart';
import 'package:freegosy/core/storage/download_cache_service.dart';
import 'package:freegosy/core/storage/metadata_cache_service.dart';
+import 'package:freegosy/core/storage/rom_mapping_service.dart';
+import 'package:freegosy/core/romm/rom_scanner_service.dart';
import 'package:freegosy/core/emulator/firmware_service.dart';
@@ -164,3 +166,19 @@ final metadataCacheServiceProvider = FutureProvider<MetadataCacheService>((ref)
await service.load();
return service;
});
+
+final romMappingServiceProvider = FutureProvider<RomMappingService>((ref) async {
+ final service = RomMappingService();
+ await service.init();
+ return service;
+});
+
+final romScannerServiceProvider = Provider<RomScannerService?>((ref) {
+ final rommService = ref.watch(rommServiceProvider);
+ final mappingServiceAsync = ref.watch(romMappingServiceProvider);
+
+ if (rommService != null && mappingServiceAsync.hasValue) {
+ return RomScannerService(rommService, mappingServiceAsync.value!);
+ }
+ return null;
+});
diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart
index e28893c..3b62696 100644
--- a/lib/ui/screens/library_screen.dart
+++ b/lib/ui/screens/library_screen.dart
@@ -150,7 +150,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
final rommConfigAsync = ref.watch(rommConfigProvider);
final directoryServiceAsync = ref.watch(directoryServiceProvider);
final downloadedCache = ref.watch(downloadedGamesCacheProvider);
- final isDeepScanning = ref.watch(downloadedGamesCacheProvider.notifier).isDeepScanning;
+ final isSyncing = ref.watch(downloadedGamesCacheProvider.notifier).isSyncing;
// Trigger initial load once service becomes available
ref.listen(rommServiceProvider, (prev, next) {
@@ -249,7 +249,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
child: ExcludeSemantics(
child: Column(
children: [
- if (isDeepScanning)
+ if (isSyncing)
const LinearProgressIndicator(
minHeight: 2,
backgroundColor: Colors.transparent,
diff --git a/pubspec.lock b/pubspec.lock
index 959ec29..32610cd 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -424,6 +424,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
+ hive:
+ dependency: "direct main"
+ description:
+ name: hive
+ sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.3"
+ hive_flutter:
+ dependency: "direct main"
+ description:
+ name: hive_flutter
+ sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
hooks:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 24c5d80..f240786 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -48,6 +48,8 @@ dependencies:
path: ^1.9.0
flutter_secure_storage: ^9.2.4
url_launcher: ^6.3.2
+ hive: ^2.2.3
+ hive_flutter: ^1.1.0
dev_dependencies:
flutter_test:
diff --git a/test/unit/firmware_service_test.mocks.dart b/test/unit/firmware_service_test.mocks.dart
index 39ed124..8903990 100644
--- a/test/unit/firmware_service_test.mocks.dart
+++ b/test/unit/firmware_service_test.mocks.dart
@@ -145,6 +145,22 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<List<_i2.Game>>);
+ @override
+ _i7.Future<List<_i2.Game>> searchRoms({
+ String? sha1,
+ String? md5,
+ String? search,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#searchRoms, [], {
+ #sha1: sha1,
+ #md5: md5,
+ #search: search,
+ }),
+ returnValue: _i7.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+ )
+ as _i7.Future<List<_i2.Game>>);
+
@override
_i7.Future<({List<_i2.Game> games, int total})> getGamesPage({
int? offset = 0,
@@ -231,6 +247,14 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<bool>);
+ @override
+ _i7.Future<bool> deleteSaves(List<int>? saveIds) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteSaves, [saveIds]),
+ returnValue: _i7.Future<bool>.value(false),
+ )
+ as _i7.Future<bool>);
+
@override
_i7.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
(super.noSuchMethod(
diff --git a/test/unit/paginated_games_provider_test.mocks.dart b/test/unit/paginated_games_provider_test.mocks.dart
index 09e9203..6e7b987 100644
--- a/test/unit/paginated_games_provider_test.mocks.dart
+++ b/test/unit/paginated_games_provider_test.mocks.dart
@@ -129,6 +129,22 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
)
as _i5.Future<List<_i2.Game>>);
+ @override
+ _i5.Future<List<_i2.Game>> searchRoms({
+ String? sha1,
+ String? md5,
+ String? search,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#searchRoms, [], {
+ #sha1: sha1,
+ #md5: md5,
+ #search: search,
+ }),
+ 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,
@@ -215,6 +231,14 @@ class MockRommService extends _i1.Mock implements _i3.RommService {
)
as _i5.Future<bool>);
+ @override
+ _i5.Future<bool> deleteSaves(List<int>? saveIds) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteSaves, [saveIds]),
+ returnValue: _i5.Future<bool>.value(false),
+ )
+ as _i5.Future<bool>);
+
@override
_i5.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
(super.noSuchMethod(
diff --git a/test/unit/save_sync_service_test.mocks.dart b/test/unit/save_sync_service_test.mocks.dart
index 73a7aa7..9d651f3 100644
--- a/test/unit/save_sync_service_test.mocks.dart
+++ b/test/unit/save_sync_service_test.mocks.dart
@@ -145,6 +145,22 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<List<_i2.Game>>);
+ @override
+ _i7.Future<List<_i2.Game>> searchRoms({
+ String? sha1,
+ String? md5,
+ String? search,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#searchRoms, [], {
+ #sha1: sha1,
+ #md5: md5,
+ #search: search,
+ }),
+ returnValue: _i7.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+ )
+ as _i7.Future<List<_i2.Game>>);
+
@override
_i7.Future<({List<_i2.Game> games, int total})> getGamesPage({
int? offset = 0,
@@ -231,6 +247,14 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<bool>);
+ @override
+ _i7.Future<bool> deleteSaves(List<int>? saveIds) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteSaves, [saveIds]),
+ returnValue: _i7.Future<bool>.value(false),
+ )
+ as _i7.Future<bool>);
+
@override
_i7.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
(super.noSuchMethod(
diff --git a/test/widgets/library_screen_test.dart b/test/widgets/library_screen_test.dart
index 36461e4..bccd216 100644
--- a/test/widgets/library_screen_test.dart
+++ b/test/widgets/library_screen_test.dart
@@ -10,21 +10,27 @@ import 'package:freegosy/ui/screens/library_screen.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
+import 'package:freegosy/core/storage/rom_mapping_service.dart';
import 'library_screen_test.mocks.dart';
-@GenerateMocks([RommService, DirectoryService])
+@GenerateMocks([RommService, DirectoryService, RomMappingService])
void main() {
late MockRommService mockRommService;
late MockDirectoryService mockDirectoryService;
+ late MockRomMappingService mockRomMappingService;
setUp(() {
mockRommService = MockRommService();
mockDirectoryService = MockDirectoryService();
+ mockRomMappingService = MockRomMappingService();
when(mockRommService.config).thenReturn(RomMConfig(baseUrl: 'https://test.com', username: 'u', password: 'p'));
when(mockRommService.resolveCoverUrl(any)).thenReturn(null);
when(mockRommService.getRecentlyPlayed(limit: anyNamed('limit'))).thenAnswer((_) async => []);
+ when(mockRommService.searchRoms(search: anyNamed('search'))).thenAnswer((_) async => []);
when(mockDirectoryService.status).thenReturn(const StorageStatus());
+ when(mockRomMappingService.getMappings()).thenReturn({});
+ when(mockRomMappingService.getMTimes()).thenReturn({});
});
group('LibraryScreen', () {
@@ -34,6 +40,8 @@ void main() {
await tester.pumpWidget(ProviderScope(
overrides: [
rommServiceProvider.overrideWithValue(mockRommService),
+ romMappingServiceProvider.overrideWith((ref) => Future.value(mockRomMappingService)),
+ romScannerServiceProvider.overrideWithValue(null),
directoryServiceProvider.overrideWith((ref) => Future.value(mockDirectoryService)),
paginatedGamesProvider.overrideWith((ref) => PaginatedGamesNotifier(ref)..state = const PaginatedGamesState(isLoading: true)),
],
@@ -57,6 +65,8 @@ void main() {
await tester.pumpWidget(ProviderScope(
overrides: [
rommServiceProvider.overrideWithValue(mockRommService),
+ romMappingServiceProvider.overrideWith((ref) => Future.value(mockRomMappingService)),
+ romScannerServiceProvider.overrideWithValue(null),
directoryServiceProvider.overrideWith((ref) => Future.value(mockDirectoryService)),
paginatedGamesProvider.overrideWith((ref) => PaginatedGamesNotifier(ref)..state = PaginatedGamesState(games: games, total: 2, hasMore: false)),
],
@@ -77,6 +87,8 @@ void main() {
await tester.pumpWidget(ProviderScope(
overrides: [
rommServiceProvider.overrideWithValue(mockRommService),
+ romMappingServiceProvider.overrideWith((ref) => Future.value(mockRomMappingService)),
+ romScannerServiceProvider.overrideWithValue(null),
directoryServiceProvider.overrideWith((ref) => Future.value(mockDirectoryService)),
paginatedGamesProvider.overrideWith((ref) => PaginatedGamesNotifier(ref)..state = const PaginatedGamesState(games: [], total: 0, hasMore: false)),
],
@@ -95,6 +107,8 @@ void main() {
await tester.pumpWidget(ProviderScope(
overrides: [
rommServiceProvider.overrideWithValue(mockRommService),
+ romMappingServiceProvider.overrideWith((ref) => Future.value(mockRomMappingService)),
+ romScannerServiceProvider.overrideWithValue(null),
directoryServiceProvider.overrideWith((ref) => Future.value(mockDirectoryService)),
paginatedGamesProvider.overrideWith((ref) => PaginatedGamesNotifier(ref)..state = const PaginatedGamesState(error: 'Connection Failed')),
],
diff --git a/test/widgets/library_screen_test.mocks.dart b/test/widgets/library_screen_test.mocks.dart
index 5c695ba..1f326cc 100644
--- a/test/widgets/library_screen_test.mocks.dart
+++ b/test/widgets/library_screen_test.mocks.dart
@@ -12,6 +12,7 @@ import 'package:freegosy/core/emulator/linux_strategies/linux_environment_strate
import 'package:freegosy/core/romm/romm_models.dart' as _i2;
import 'package:freegosy/core/romm/romm_service.dart' as _i5;
import 'package:freegosy/core/storage/directory_service.dart' as _i3;
+import 'package:freegosy/core/storage/rom_mapping_service.dart' as _i10;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i6;
@@ -143,6 +144,22 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<List<_i2.Game>>);
+ @override
+ _i7.Future<List<_i2.Game>> searchRoms({
+ String? sha1,
+ String? md5,
+ String? search,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#searchRoms, [], {
+ #sha1: sha1,
+ #md5: md5,
+ #search: search,
+ }),
+ returnValue: _i7.Future<List<_i2.Game>>.value(<_i2.Game>[]),
+ )
+ as _i7.Future<List<_i2.Game>>);
+
@override
_i7.Future<({List<_i2.Game> games, int total})> getGamesPage({
int? offset = 0,
@@ -229,6 +246,14 @@ class MockRommService extends _i1.Mock implements _i5.RommService {
)
as _i7.Future<bool>);
+ @override
+ _i7.Future<bool> deleteSaves(List<int>? saveIds) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteSaves, [saveIds]),
+ returnValue: _i7.Future<bool>.value(false),
+ )
+ as _i7.Future<bool>);
+
@override
_i7.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
(super.noSuchMethod(
@@ -857,3 +882,87 @@ class MockDirectoryService extends _i1.Mock implements _i3.DirectoryService {
)
as _i7.Future<void>);
}
+
+/// A class which mocks [RomMappingService].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockRomMappingService extends _i1.Mock implements _i10.RomMappingService {
+ MockRomMappingService() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i7.Future<void> init() =>
+ (super.noSuchMethod(
+ Invocation.method(#init, []),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+
+ @override
+ Map<String, String> getMappings() =>
+ (super.noSuchMethod(
+ Invocation.method(#getMappings, []),
+ returnValue: <String, String>{},
+ )
+ as Map<String, String>);
+
+ @override
+ _i7.Future<void> saveMappings(Map<String, String>? mappings) =>
+ (super.noSuchMethod(
+ Invocation.method(#saveMappings, [mappings]),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+
+ @override
+ _i7.Future<void> updateMapping(String? path, String? romId) =>
+ (super.noSuchMethod(
+ Invocation.method(#updateMapping, [path, romId]),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+
+ @override
+ Map<String, int> getMTimes() =>
+ (super.noSuchMethod(
+ Invocation.method(#getMTimes, []),
+ returnValue: <String, int>{},
+ )
+ as Map<String, int>);
+
+ @override
+ _i7.Future<void> saveMTimes(Map<String, int>? mtimes) =>
+ (super.noSuchMethod(
+ Invocation.method(#saveMTimes, [mtimes]),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+
+ @override
+ _i7.Future<void> setLastSyncTime(int? timestamp) =>
+ (super.noSuchMethod(
+ Invocation.method(#setLastSyncTime, [timestamp]),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+
+ @override
+ String? getRomIdForPath(String? path) =>
+ (super.noSuchMethod(Invocation.method(#getRomIdForPath, [path]))
+ as String?);
+
+ @override
+ _i7.Future<void> clear() =>
+ (super.noSuchMethod(
+ Invocation.method(#clear, []),
+ returnValue: _i7.Future<void>.value(),
+ returnValueForMissingStub: _i7.Future<void>.value(),
+ )
+ as _i7.Future<void>);
+}
diff --git a/test/widgets/settings_screen_test.mocks.dart b/test/widgets/settings_screen_test.mocks.dart
index db64fda..0e4c4d6 100644
--- a/test/widgets/settings_screen_test.mocks.dart
+++ b/test/widgets/settings_screen_test.mocks.dart
@@ -647,6 +647,22 @@ class MockRommService extends _i1.Mock implements _i8.RommService {
)
as _i6.Future<List<_i4.Game>>);
+ @override
+ _i6.Future<List<_i4.Game>> searchRoms({
+ String? sha1,
+ String? md5,
+ String? search,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#searchRoms, [], {
+ #sha1: sha1,
+ #md5: md5,
+ #search: search,
+ }),
+ returnValue: _i6.Future<List<_i4.Game>>.value(<_i4.Game>[]),
+ )
+ as _i6.Future<List<_i4.Game>>);
+
@override
_i6.Future<({List<_i4.Game> games, int total})> getGamesPage({
int? offset = 0,
@@ -733,6 +749,14 @@ class MockRommService extends _i1.Mock implements _i8.RommService {
)
as _i6.Future<bool>);
+ @override
+ _i6.Future<bool> deleteSaves(List<int>? saveIds) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteSaves, [saveIds]),
+ returnValue: _i6.Future<bool>.value(false),
+ )
+ as _i6.Future<bool>);
+
@override
_i6.Future<void> pruneOldSaves(String? gameId, {int? keepCount = 5}) =>
(super.noSuchMethod(