-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit fe485a5
abduznik edited this page May 23, 2026
·
1 revision
Commit: fe485a5db1b728c49b6579957263033068140d6d
Author: Yan
Date: 2026年04月13日
Why: Major implementation or integration of a feature set.
lib/core/romm/romm_models.dart | 25 +++++++++
lib/core/storage/metadata_cache_service.dart | 84 ++++++++++++++++++++++++++++
lib/providers/paginated_games_provider.dart | 51 ++++++++++++++++-
lib/providers/romm_provider.dart | 7 +++
lib/ui/screens/library_screen.dart | 66 ++++++++++++++++++++--
5 files changed, 226 insertions(+), 7 deletions(-)
lib/core/romm/romm_models.dartlib/core/storage/metadata_cache_service.dartlib/providers/paginated_games_provider.dartlib/providers/romm_provider.dartlib/ui/screens/library_screen.dart
diff --git a/lib/core/romm/romm_models.dart b/lib/core/romm/romm_models.dart index e059537..5190458 100644 --- a/lib/core/romm/romm_models.dart +++ b/lib/core/romm/romm_models.dart @@ -148,6 +148,31 @@ class Game { 'file_size_bytes': fileSize, 'multi_file_path': multiFilePath, 'has_multiple_files': hasMultipleFiles, + 'files': files, + 'summary': summary, + 'metadatum': { + 'genres': genres, + 'companies': companies, + 'player_count': playerCount, + 'first_release_date': firstReleaseDate, + 'average_rating': averageRating, + }, + 'regions': regions, + 'languages': languages, + 'tags': tags, + 'merged_screenshots': mergedScreenshots, + 'ss_metadata': { + 'screenshot_url': screenshotUrl, + 'fanart_url': fanartUrl, + }, + 'rom_user': { + 'last_played': lastPlayed?.toIso8601String(), + 'rating': userRating, + 'completion': completion, + 'status': status, + 'backlogged': backlogged, + 'now_playing': nowPlaying, + }, }; } } diff --git a/lib/core/storage/metadata_cache_service.dart b/lib/core/storage/metadata_cache_service.dart new file mode 100644 index 0000000..8296488 --- /dev/null +++ b/lib/core/storage/metadata_cache_service.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../romm/romm_models.dart'; + +class MetadataCacheService { + static const String _gamesKey = 'cached_games_metadata'; + List<Game> _cachedGames = []; + + MetadataCacheService(); + + List<Game> get cachedGames => _cachedGames; + + Future<void> load() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_gamesKey); + if (jsonStr == null) return; + + final List<dynamic> decoded = jsonDecode(jsonStr); + _cachedGames = decoded.map((json) => Game.fromJson(json)).toList(); + } catch (_) { + _cachedGames = []; + } + } + + Future<void> saveGames(List<Game> games) async { + // Merge new games with existing cache, avoiding duplicates by ID + final Map<String, Game> gameMap = {for (var g in _cachedGames) g.id: g}; + for (var g in games) { + gameMap[g.id] = g; + } + _cachedGames = gameMap.values.toList(); + await _persist(); + } + + Future<void> _persist() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonList = _cachedGames.map((g) => g.toJson()).toList(); + await prefs.setString(_gamesKey, jsonEncode(jsonList)); + } catch (_) {} + } + + List<Game> getOfflineGames({ + String? platformId, + String? search, + List<String>? genres, + List<String>? regions, + List<String>? languages, + }) { + return _cachedGames.where((game) { + // Platform filter + if (platformId != null && game.platformId.toString() != platformId) { + return false; + } + + // Search filter + if (search != null && search.isNotEmpty) { + final query = search.toLowerCase(); + if (!game.name.toLowerCase().contains(query) && + !(game.platformDisplayName?.toLowerCase().contains(query) ?? false)) { + return false; + } + } + + // Genre filter + if (genres != null && genres.isNotEmpty) { + if (!genres.any((g) => game.genres.contains(g))) return false; + } + + // Region filter + if (regions != null && regions.isNotEmpty) { + if (!regions.any((r) => game.regions.contains(r))) return false; + } + + // Language filter + if (languages != null && languages.isNotEmpty) { + if (!languages.any((l) => game.languages.contains(l))) return false; + } + + return true; + }).toList(); + } +} diff --git a/lib/providers/paginated_games_provider.dart b/lib/providers/paginated_games_provider.dart index 0d1f03f..c571b3b 100644 --- a/lib/providers/paginated_games_provider.dart +++ b/lib/providers/paginated_games_provider.dart @@ -137,10 +137,13 @@ class PaginatedGamesNotifier extends StateNotifier<PaginatedGamesState> { // No cache — show loading and fetch state = const PaginatedGamesState(isLoading: true); final service = _ref.read(rommServiceProvider); + if (service == null) { - state = state.copyWith(isLoading: false, error: 'Not connected'); + // Fallback to offline cache + await _loadOffline(platformId, search); return; } + try { final result = await service.getGamesPage( offset: 0, @@ -153,6 +156,7 @@ class PaginatedGamesNotifier extends StateNotifier<PaginatedGamesState> { collections: _activeFilters.collections, statuses: _activeFilters.statuses, ); + _cache[key] = result.games; _offsets[key] = result.games.length; _totals[key] = result.total; @@ -162,11 +166,42 @@ class PaginatedGamesNotifier extends StateNotifier<PaginatedGamesState> { hasMore: result.games.length < result.total, isLoading: false, ); + + // Persist results for offline use + final cacheService = _ref.read(metadataCacheServiceProvider).value; + if (cacheService != null) { + await cacheService.saveGames(result.games); + } } catch (e) { - state = state.copyWith(isLoading: false, error: e.toString()); + // On error, also try fallback + await _loadOffline(platformId, search, error: e.toString()); } } + Future<void> _loadOffline(String? platformId, String? search, {String? error}) async { + final cacheService = _ref.read(metadataCacheServiceProvider).value; + if (cacheService == null) { + state = state.copyWith(isLoading: false, error: error ?? 'Not connected'); + return; + } + + final offlineGames = cacheService.getOfflineGames( + platformId: platformId, + search: search, + genres: _activeFilters.genres, + regions: _activeFilters.regions, + languages: _activeFilters.languages, + ); + + state = PaginatedGamesState( + games: offlineGames, + total: offlineGames.length, + hasMore: false, + isLoading: false, + error: error != null ? 'Offline Mode (Server: $error)' : 'Offline Mode', + ); + } + Future<void> _backgroundRefresh({ required String? platformId, required String? search, @@ -197,6 +232,12 @@ class PaginatedGamesNotifier extends StateNotifier<PaginatedGamesState> { hasMore: result.games.length < result.total, isLoading: false, ); + + // Update cache + final cacheService = _ref.read(metadataCacheServiceProvider).value; + if (cacheService != null) { + await cacheService.saveGames(result.games); + } } } catch (_) {} } @@ -230,6 +271,12 @@ class PaginatedGamesNotifier extends StateNotifier<PaginatedGamesState> { hasMore: merged.length < result.total, isLoadingMore: false, ); + + // Update cache + final cacheService = _ref.read(metadataCacheServiceProvider).value; + if (cacheService != null) { + await cacheService.saveGames(result.games); + } } catch (e) { state = state.copyWith(isLoadingMore: false, error: e.toString()); } diff --git a/lib/providers/romm_provider.dart b/lib/providers/romm_provider.dart index f7eba4d..a7bd07f 100644 --- a/lib/providers/romm_provider.dart +++ b/lib/providers/romm_provider.dart @@ -10,6 +10,7 @@ import 'package:freegosy/core/emulator/strategies/windows_strategy.dart'; 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/emulator/firmware_service.dart'; @@ -139,3 +140,9 @@ final rommServiceProvider = Provider<RommService?>((ref) { } return null; }); + +final metadataCacheServiceProvider = FutureProvider<MetadataCacheService>((ref) async { + final service = MetadataCacheService(); + await service.load(); + return service; +}); diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 9032605..cbec958 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -446,15 +446,71 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio ], ), ), + if (paginatedState.error != null && paginatedState.error!.contains('Offline Mode')) + Container( + color: Colors.orange.withValues(alpha: 0.1), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Icon(Icons.cloud_off, color: Colors.orange), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Offline Mode', + style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold), + ), + Text( + paginatedState.error!, + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + ], + ), + ), + TextButton( + onPressed: () { + ref.invalidate(rommServiceProvider); + ref.read(paginatedGamesProvider.notifier).loadInitial( + platformId: ref.read(selectedPlatformIdProvider)?.toString(), + search: ref.read(searchQueryProvider), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), Expanded( child: paginatedState.isLoading ? buildSkeletonGrid(cardAspectRatio, columnCount, cardSpacing, context) - : paginatedState.error != null + : (paginatedState.error != null && !paginatedState.error!.contains('Offline Mode')) ? Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text('Error: ${paginatedState.error}', style: const TextStyle(color: Colors.red)), - )) + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + Text( + 'Error: ${paginatedState.error}', + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.invalidate(rommServiceProvider); + ref.read(paginatedGamesProvider.notifier).loadInitial( + platformId: ref.read(selectedPlatformIdProvider)?.toString(), + search: ref.read(searchQueryProvider), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ) : Column( children: [ Padding(