-
-
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(