-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit 29994a0
abduznik edited this page May 23, 2026
·
1 revision
Commit: 29994a04f7d7b51c928f3d7bbdee8f8d503597e9
Author: abduznik
Date: 2026-05-01
Why: Adds a new feature or capability to the application.
lib/core/romm/library_snapshot_service.dart | 78 +++++++++++++++++++++++
lib/core/romm/romm_models.dart | 32 ++++++++++
lib/core/romm/romm_service.dart | 25 +++++++-
lib/core/storage/metadata_cache_service.dart | 94 +++++++++++++++++++++++-----
4 files changed, 211 insertions(+), 18 deletions(-)
lib/core/romm/library_snapshot_service.dartlib/core/romm/romm_models.dartlib/core/romm/romm_service.dartlib/core/storage/metadata_cache_service.dart
diff --git a/lib/core/romm/library_snapshot_service.dart b/lib/core/romm/library_snapshot_service.dart
new file mode 100644
index 0000000..cbdefa8
--- /dev/null
+++ b/lib/core/romm/library_snapshot_service.dart
@@ -0,0 +1,78 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:path_provider/path_provider.dart';
+import 'package:flutter/foundation.dart';
+import 'romm_models.dart';
+
+class LibrarySnapshotService {
+ static const String _platformsFile = 'platforms_snapshot.json';
+ static const String _collectionsFile = 'collections_snapshot.json';
+
+ Future<String> _getFilePath(String fileName) async {
+ final directory = await getApplicationDocumentsDirectory();
+ return '${directory.path}/$fileName';
+ }
+
+ Future<void> savePlatforms(List<Platform> platforms) async {
+ try {
+ final path = await _getFilePath(_platformsFile);
+ final jsonStr = jsonEncode(platforms.map((p) => p.toJson()).toList());
+ await File(path).writeAsString(jsonStr);
+ } catch (e) {
+ debugPrint('[LibrarySnapshot] Error saving platforms: $e');
+ }
+ }
+
+ Future<List<Platform>> loadPlatforms() async {
+ try {
+ final path = await _getFilePath(_platformsFile);
+ final file = File(path);
+ if (!await file.exists()) return [];
+
+ final jsonStr = await file.readAsString();
+ final List<dynamic> decoded = jsonDecode(jsonStr);
+ return decoded.map((json) => Platform.fromJson(json)).toList();
+ } catch (e) {
+ debugPrint('[LibrarySnapshot] Error loading platforms: $e');
+ return [];
+ }
+ }
+
+ Future<void> saveCollections(List<Map<String, dynamic>> collections) async {
+ try {
+ final path = await _getFilePath(_collectionsFile);
+ final jsonStr = jsonEncode(collections);
+ await File(path).writeAsString(jsonStr);
+ } catch (e) {
+ debugPrint('[LibrarySnapshot] Error saving collections: $e');
+ }
+ }
+
+ Future<List<Map<String, dynamic>>> loadCollections() async {
+ try {
+ final path = await _getFilePath(_collectionsFile);
+ final file = File(path);
+ if (!await file.exists()) return [];
+
+ final jsonStr = await file.readAsString();
+ final List<dynamic> decoded = jsonDecode(jsonStr);
+ return decoded.map((e) => e as Map<String, dynamic>).toList();
+ } catch (e) {
+ debugPrint('[LibrarySnapshot] Error loading collections: $e');
+ return [];
+ }
+ }
+
+ Future<void> clear() async {
+ try {
+ final pPath = await _getFilePath(_platformsFile);
+ final cPath = await _getFilePath(_collectionsFile);
+ final pFile = File(pPath);
+ final cFile = File(cPath);
+ if (await pFile.exists()) await pFile.delete();
+ if (await cFile.exists()) await cFile.delete();
+ } catch (e) {
+ debugPrint('[LibrarySnapshot] Error clearing snapshots: $e');
+ }
+ }
+}
diff --git a/lib/core/romm/romm_models.dart b/lib/core/romm/romm_models.dart
index e89c5fc..72063c6 100644
--- a/lib/core/romm/romm_models.dart
+++ b/lib/core/romm/romm_models.dart
@@ -238,6 +238,25 @@ class Firmware {
updatedAt: json['updated_at'] != null ? DateTime.tryParse(json['updated_at'].toString()) : null,
);
}
+
+ Map<String, dynamic> toJson() {
+ return {
+ 'id': id,
+ 'file_name': fileName,
+ 'file_name_no_tags': fileNameNoTags,
+ 'file_name_no_ext': fileNameNoExt,
+ 'file_extension': fileExtension,
+ 'file_path': filePath,
+ 'file_size_bytes': fileSizeBytes,
+ 'is_verified': isVerified,
+ 'crc_hash': crcHash,
+ 'md5_hash': md5Hash,
+ 'sha1_hash': sha1Hash,
+ 'missing_from_fs': missingFromFs,
+ 'created_at': createdAt?.toIso8601String(),
+ 'updated_at': updatedAt?.toIso8601String(),
+ };
+ }
}
class Platform {
@@ -277,6 +296,19 @@ class Platform {
firmwareCount: json['firmware_count'] as int? ?? 0,
);
}
+
+ Map<String, dynamic> toJson() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'slug': slug,
+ 'fs_slug': fsSlug,
+ 'display_name': displayName,
+ 'rom_count': gamesCount,
+ 'firmware': firmware.map((e) => e.toJson()).toList(),
+ 'firmware_count': firmwareCount,
+ };
+ }
}
class RomNote {
diff --git a/lib/core/romm/romm_service.dart b/lib/core/romm/romm_service.dart
index f8bceb2..d6bd79c 100644
--- a/lib/core/romm/romm_service.dart
+++ b/lib/core/romm/romm_service.dart
@@ -1,13 +1,13 @@
import 'dart:convert';
import 'dart:io' as io;
import 'dart:math';
+import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:shared_preferences/shared_preferences.dart';
import '../storage/secure_storage_service.dart';
import 'romm_models.dart';
-import 'dart:async';
class RommService {
RomMConfig _config;
@@ -147,6 +147,9 @@ class RommService {
final headers = <String, dynamic>{};
if (config.apiKey.isNotEmpty) {
+ // Send both headers for maximum compatibility across all RomM versions and proxies.
+ // Standard API Keys often work via X-Api-Key, while Client Tokens often require Bearer.
+ // Many setups use the API Key in the Bearer field, so we provide both to be safe.
headers['Authorization'] = 'Bearer ${config.apiKey}';
headers['X-Api-Key'] = config.apiKey;
} else if (config.token != null && config.token!.isNotEmpty) {
@@ -182,6 +185,26 @@ class RommService {
return token;
}
+ static Future<String> exchangePairingCode(String baseUrl, String code) async {
+ final normalizedUrl = _normalizeBaseUrl(baseUrl);
+ final dio = Dio(BaseOptions(
+ baseUrl: normalizedUrl,
+ connectTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 10),
+ headers: {'User-Agent': _ua},
+ ));
+
+ final response = await dio.post(
+ '/api/client-tokens/exchange',
+ data: {'code': code},
+ );
+
+ final token = response.data['raw_token'] as String?;
+ if (token == null || token.isEmpty) throw Exception('Pairing failed: no token in response');
+
+ return token;
+ }
+
Future<void> refreshToken(SharedPreferences prefs) async {
try {
if (_config.username.isEmpty || _config.password.isEmpty) return;
diff --git a/lib/core/storage/metadata_cache_service.dart b/lib/core/storage/metadata_cache_service.dart
index 3828ab7..1d64b12 100644
--- a/lib/core/storage/metadata_cache_service.dart
+++ b/lib/core/storage/metadata_cache_service.dart
@@ -1,24 +1,43 @@
import 'dart:convert';
-import 'package:shared_preferences/shared_preferences.dart';
+import 'dart:io';
+import 'package:path_provider/path_provider.dart';
+import 'package:flutter/foundation.dart';
import '../romm/romm_models.dart';
class MetadataCacheService {
- static const String _gamesKey = 'cached_games_metadata';
- final SharedPreferences _prefs;
+ static const String _gamesFile = 'games_cache.json';
+ static const String _countsFile = 'platform_counts.json';
+
List<Game> _cachedGames = [];
-
- MetadataCacheService(this._prefs);
+ Map<String, int> _platformCounts = {};
List<Game> get cachedGames => _cachedGames;
- void load() {
+ MetadataCacheService();
+
+ Future<String> _getFilePath(String fileName) async {
+ final directory = await getApplicationDocumentsDirectory();
+ return '${directory.path}/$fileName';
+ }
+
+ Future<void> load() async {
try {
- final jsonStr = _prefs.getString(_gamesKey);
- if (jsonStr == null) return;
-
- final List<dynamic> decoded = jsonDecode(jsonStr);
- _cachedGames = decoded.map((json) => Game.fromJson(json)).toList();
- } catch (_) {
+ final gamesPath = await _getFilePath(_gamesFile);
+ final gamesFile = File(gamesPath);
+ if (await gamesFile.exists()) {
+ final jsonStr = await gamesFile.readAsString();
+ final List<dynamic> decoded = jsonDecode(jsonStr);
+ _cachedGames = decoded.map((json) => Game.fromJson(json)).toList();
+ }
+
+ final countsPath = await _getFilePath(_countsFile);
+ final countsFile = File(countsPath);
+ if (await countsFile.exists()) {
+ final jsonStr = await countsFile.readAsString();
+ _platformCounts = Map<String, int>.from(jsonDecode(jsonStr));
+ }
+ } catch (e) {
+ debugPrint('[MetadataCache] Error loading cache: $e');
_cachedGames = [];
}
}
@@ -30,14 +49,42 @@ class MetadataCacheService {
gameMap[g.id] = g;
}
_cachedGames = gameMap.values.toList();
- _persist();
+ await _persistGames();
+ }
+
+ Future<void> updatePlatformCount(String platformId, int count) async {
+ _platformCounts[platformId] = count;
+ await _persistCounts();
}
- void _persist() {
+ bool isPlatformValid(String platformId, int remoteCount) {
+ return _platformCounts[platformId] == remoteCount;
+ }
+
+ Future<void> invalidatePlatform(String platformId) async {
+ _cachedGames.removeWhere((g) => g.platformId.toString() == platformId);
+ _platformCounts.remove(platformId);
+ await _persistGames();
+ await _persistCounts();
+ }
+
+ Future<void> _persistGames() async {
try {
- final jsonList = _cachedGames.map((g) => g.toJson()).toList();
- _prefs.setString(_gamesKey, jsonEncode(jsonList));
- } catch (_) {}
+ final path = await _getFilePath(_gamesFile);
+ final jsonStr = jsonEncode(_cachedGames.map((g) => g.toJson()).toList());
+ await File(path).writeAsString(jsonStr);
+ } catch (e) {
+ debugPrint('[MetadataCache] Error persisting games: $e');
+ }
+ }
+
+ Future<void> _persistCounts() async {
+ try {
+ final path = await _getFilePath(_countsFile);
+ await File(path).writeAsString(jsonEncode(_platformCounts));
+ } catch (e) {
+ debugPrint('[MetadataCache] Error persisting counts: $e');
+ }
}
List<Game> getOfflineGames({
@@ -80,4 +127,17 @@ class MetadataCacheService {
return true;
}).toList();
}
+
+ Future<void> clear() async {
+ _cachedGames = [];
+ _platformCounts = {};
+ try {
+ final gPath = await _getFilePath(_gamesFile);
+ final cPath = await _getFilePath(_countsFile);
+ final gFile = File(gPath);
+ final cFile = File(cPath);
+ if (await gFile.exists()) await gFile.delete();
+ if (await cFile.exists()) await cFile.delete();
+ } catch (_) {}
+ }
}