some fixes

This commit is contained in:
gitea
2025-11-06 15:09:41 +01:00
parent f8aa20eb5c
commit d68124d975
13 changed files with 152 additions and 102 deletions
+37 -31
View File
@@ -7,7 +7,6 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import '../local/local_storage_service.dart';
import '../local/models/item.dart';
import '../session/models/user.dart';
import 'models/firebase_config.dart';
/// Exception thrown when Firebase operations fail.
@@ -23,17 +22,17 @@ class FirebaseException implements Exception {
}
/// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics).
///
///
/// This service provides:
/// - Cloud Firestore for optional metadata sync and backup
/// - Firebase Storage for optional media storage
/// - Firebase Authentication for user login/logout
/// - Firebase Cloud Messaging for push notifications
/// - Firebase Analytics for optional analytics
///
///
/// The service is modular and optional - can be enabled/disabled without affecting other modules.
/// When disabled, all methods return safely without throwing errors.
///
///
/// The service maintains offline-first behavior by syncing with local storage
/// and only using Firebase as an optional cloud backup/sync layer.
class FirebaseService {
@@ -65,7 +64,7 @@ class FirebaseService {
firebase_auth.User? _firebaseUser;
/// Creates a [FirebaseService] instance.
///
///
/// [config] - Firebase configuration (determines which services are enabled).
/// [localStorage] - Local storage service for offline-first behavior.
FirebaseService({
@@ -83,10 +82,10 @@ class FirebaseService {
bool get isLoggedIn => _auth != null && _firebaseUser != null;
/// Initializes Firebase services based on configuration.
///
///
/// Must be called before using any Firebase services.
/// If Firebase is disabled, this method does nothing.
///
///
/// Throws [FirebaseException] if initialization fails.
Future<void> initialize() async {
if (!config.enabled) {
@@ -141,12 +140,12 @@ class FirebaseService {
}
/// Logs in a user with email and password.
///
///
/// [email] - User email address.
/// [password] - User password.
///
///
/// Returns the Firebase Auth user.
///
///
/// Throws [FirebaseException] if auth is disabled or login fails.
Future<firebase_auth.User> loginWithEmailPassword({
required String email,
@@ -157,7 +156,8 @@ class FirebaseService {
}
if (!_initialized || _auth == null) {
throw FirebaseException('Firebase not initialized. Call initialize() first.');
throw FirebaseException(
'Firebase not initialized. Call initialize() first.');
}
try {
@@ -173,7 +173,7 @@ class FirebaseService {
}
/// Logs out the current user.
///
///
/// Throws [FirebaseException] if auth is disabled or logout fails.
Future<void> logout() async {
if (!config.enabled || !config.authEnabled) {
@@ -181,7 +181,8 @@ class FirebaseService {
}
if (!_initialized || _auth == null) {
throw FirebaseException('Firebase not initialized. Call initialize() first.');
throw FirebaseException(
'Firebase not initialized. Call initialize() first.');
}
try {
@@ -193,9 +194,9 @@ class FirebaseService {
}
/// Syncs local items to Firestore (cloud backup).
///
///
/// [userId] - User ID to associate items with (for multi-user support).
///
///
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
Future<void> syncItemsToFirestore(String userId) async {
if (!config.enabled || !config.firestoreEnabled) {
@@ -203,7 +204,8 @@ class FirebaseService {
}
if (!_initialized || _firestore == null) {
throw FirebaseException('Firestore not initialized. Call initialize() first.');
throw FirebaseException(
'Firestore not initialized. Call initialize() first.');
}
try {
@@ -212,7 +214,8 @@ class FirebaseService {
// Batch write to Firestore
final batch = _firestore!.batch();
final collection = _firestore!.collection('users').doc(userId).collection('items');
final collection =
_firestore!.collection('users').doc(userId).collection('items');
for (final item in items) {
final docRef = collection.doc(item.id);
@@ -231,9 +234,9 @@ class FirebaseService {
}
/// Syncs items from Firestore to local storage.
///
///
/// [userId] - User ID to fetch items for.
///
///
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
Future<void> syncItemsFromFirestore(String userId) async {
if (!config.enabled || !config.firestoreEnabled) {
@@ -241,7 +244,8 @@ class FirebaseService {
}
if (!_initialized || _firestore == null) {
throw FirebaseException('Firestore not initialized. Call initialize() first.');
throw FirebaseException(
'Firestore not initialized. Call initialize() first.');
}
try {
@@ -272,12 +276,12 @@ class FirebaseService {
}
/// Uploads a file to Firebase Storage.
///
///
/// [file] - File to upload.
/// [path] - Storage path (e.g., 'users/userId/media/image.jpg').
///
///
/// Returns the download URL.
///
///
/// Throws [FirebaseException] if Storage is disabled or upload fails.
Future<String> uploadFile(File file, String path) async {
if (!config.enabled || !config.storageEnabled) {
@@ -285,7 +289,8 @@ class FirebaseService {
}
if (!_initialized || _storage == null) {
throw FirebaseException('Firebase Storage not initialized. Call initialize() first.');
throw FirebaseException(
'Firebase Storage not initialized. Call initialize() first.');
}
try {
@@ -298,7 +303,7 @@ class FirebaseService {
}
/// Gets the FCM token for push notifications.
///
///
/// Returns the FCM token, or null if messaging is disabled.
Future<String?> getFcmToken() async {
if (!config.enabled || !config.messagingEnabled || _messaging == null) {
@@ -313,12 +318,13 @@ class FirebaseService {
}
/// Logs an event to Firebase Analytics.
///
///
/// [eventName] - Name of the event.
/// [parameters] - Optional event parameters.
///
///
/// Does nothing if Analytics is disabled.
Future<void> logEvent(String eventName, {Map<String, dynamic>? parameters}) async {
Future<void> logEvent(String eventName,
{Map<String, dynamic>? parameters}) async {
if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
return;
}
@@ -327,7 +333,8 @@ class FirebaseService {
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
Map<String, Object>? analyticsParams;
if (parameters != null) {
analyticsParams = parameters.map((key, value) => MapEntry(key, value as Object));
analyticsParams =
parameters.map((key, value) => MapEntry(key, value as Object));
}
await _analytics!.logEvent(
@@ -340,7 +347,7 @@ class FirebaseService {
}
/// Disposes of Firebase resources.
///
///
/// Should be called when the service is no longer needed.
Future<void> dispose() async {
if (_auth != null) {
@@ -350,4 +357,3 @@ class FirebaseService {
_initialized = false;
}
}
-2
View File
@@ -175,8 +175,6 @@ class ImmichService {
}
final assetsJson = assetsData['items'] as List<dynamic>;
final total = assetsData['total'] as int? ?? 0;
final count = assetsData['count'] as int? ?? 0;
final List<ImmichAsset> assets = assetsJson
.map((json) => ImmichAsset.fromJson(json as Map<String, dynamic>))
+34 -29
View File
@@ -1,10 +1,7 @@
import 'dart:async';
import '../local/local_storage_service.dart';
import '../local/models/item.dart';
import '../immich/immich_service.dart';
import '../immich/models/immich_asset.dart';
import '../nostr/nostr_service.dart';
import '../nostr/models/nostr_event.dart';
import '../nostr/models/nostr_keypair.dart';
import 'models/sync_status.dart';
import 'models/sync_operation.dart';
@@ -22,13 +19,13 @@ class SyncException implements Exception {
}
/// Engine for coordinating data synchronization between local storage, Immich, and Nostr.
///
///
/// This service provides:
/// - Bidirectional sync between local storage, Immich, and Nostr
/// - Conflict resolution strategies
/// - Offline queue for operations when network is unavailable
/// - Automatic retry with exponential backoff
///
///
/// The service is modular and UI-independent, designed for offline-first behavior.
class SyncEngine {
/// Local storage service.
@@ -50,7 +47,8 @@ class SyncEngine {
SyncOperation? _currentOperation;
/// Stream controller for sync status updates.
final StreamController<SyncOperation> _statusController = StreamController<SyncOperation>.broadcast();
final StreamController<SyncOperation> _statusController =
StreamController<SyncOperation>.broadcast();
/// Whether the engine has been disposed.
bool _isDisposed = false;
@@ -62,7 +60,7 @@ class SyncEngine {
final int maxQueueSize;
/// Creates a [SyncEngine] instance.
///
///
/// [localStorage] - Local storage service (required).
/// [immichService] - Immich service (optional).
/// [nostrService] - Nostr service (optional).
@@ -97,7 +95,9 @@ class SyncEngine {
/// Gets the current queue of pending operations.
List<SyncOperation> getPendingOperations() {
return _operationQueue.where((op) => op.status == SyncStatus.pending).toList();
return _operationQueue
.where((op) => op.status == SyncStatus.pending)
.toList();
}
/// Gets all operations (pending, in-progress, completed, failed).
@@ -106,15 +106,15 @@ class SyncEngine {
}
/// Queues a sync operation.
///
///
/// [operation] - The sync operation to queue.
///
///
/// Throws [SyncException] if queue is full.
void queueOperation(SyncOperation operation) {
if (_isDisposed) {
throw SyncException('SyncEngine has been disposed');
}
if (_operationQueue.length >= maxQueueSize) {
throw SyncException('Sync queue is full (max: $maxQueueSize)');
}
@@ -129,12 +129,13 @@ class SyncEngine {
}
/// Syncs an item from local storage to Immich.
///
///
/// [itemId] - The ID of the item to sync.
/// [priority] - Priority of the sync operation.
///
///
/// Returns the sync operation ID.
Future<String> syncToImmich(String itemId, {SyncPriority priority = SyncPriority.normal}) async {
Future<String> syncToImmich(String itemId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_immichService == null) {
throw SyncException('Immich service not configured');
}
@@ -153,12 +154,13 @@ class SyncEngine {
}
/// Syncs metadata from Immich to local storage.
///
///
/// [assetId] - The Immich asset ID to sync.
/// [priority] - Priority of the sync operation.
///
///
/// Returns the sync operation ID.
Future<String> syncFromImmich(String assetId, {SyncPriority priority = SyncPriority.normal}) async {
Future<String> syncFromImmich(String assetId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_immichService == null) {
throw SyncException('Immich service not configured');
}
@@ -177,12 +179,13 @@ class SyncEngine {
}
/// Syncs metadata to Nostr.
///
///
/// [itemId] - The ID of the item to sync.
/// [priority] - Priority of the sync operation.
///
///
/// Returns the sync operation ID.
Future<String> syncToNostr(String itemId, {SyncPriority priority = SyncPriority.normal}) async {
Future<String> syncToNostr(String itemId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_nostrService == null) {
throw SyncException('Nostr service not configured');
}
@@ -205,11 +208,12 @@ class SyncEngine {
}
/// Performs a full sync: syncs all items between configured services.
///
///
/// [priority] - Priority of sync operations.
///
///
/// Returns a list of operation IDs.
Future<List<String>> syncAll({SyncPriority priority = SyncPriority.normal}) async {
Future<List<String>> syncAll(
{SyncPriority priority = SyncPriority.normal}) async {
final operationIds = <String>[];
// Sync local items to Immich
@@ -239,7 +243,8 @@ class SyncEngine {
/// Processes the sync queue.
Future<void> _processQueue() async {
if (_currentOperation != null || _isDisposed) return; // Already processing or disposed
if (_currentOperation != null || _isDisposed)
return; // Already processing or disposed
// Sort queue by priority (high first)
_operationQueue.sort((a, b) {
@@ -250,7 +255,8 @@ class SyncEngine {
});
// Process pending operations
while (!_isDisposed && _operationQueue.any((op) => op.status == SyncStatus.pending)) {
while (!_isDisposed &&
_operationQueue.any((op) => op.status == SyncStatus.pending)) {
final operation = _operationQueue.firstWhere(
(op) => op.status == SyncStatus.pending,
);
@@ -264,7 +270,7 @@ class SyncEngine {
operation.markSuccess();
} catch (e) {
operation.markFailed(e.toString());
// Retry if possible
if (operation.canRetry() && !_isDisposed) {
await Future.delayed(Duration(seconds: operation.retryCount));
@@ -374,10 +380,10 @@ class SyncEngine {
}
/// Resolves a conflict between local and remote data.
///
///
/// [localItem] - Local item data.
/// [remoteItem] - Remote item data.
///
///
/// Returns the resolved item data.
Map<String, dynamic> resolveConflict(
Map<String, dynamic> localItem,
@@ -421,4 +427,3 @@ class SyncEngine {
}
}
}