From fca1326f5e27c41d5fc1e8691b5d8c837d073fea Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 13 Nov 2025 20:24:43 +0100 Subject: [PATCH] bettered session and caching --- lib/core/app_initializer.dart | 11 + lib/data/blossom/blossom_service.dart | 42 +++ lib/data/immich/immich_service.dart | 42 +++ lib/data/media/media_service_interface.dart | 4 + lib/data/media/multi_media_service.dart | 18 + lib/data/recipes/recipe_service.dart | 109 +++++- lib/data/session/session_service.dart | 128 +++++++ lib/ui/favourites/favourites_screen.dart | 44 ++- .../navigation/main_navigation_scaffold.dart | 32 +- lib/ui/recipes/recipes_screen.dart | 88 ++++- .../relay_management_screen.dart | 324 +++++++++--------- lib/ui/session/session_screen.dart | 273 ++++++++++++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- pubspec.lock | 176 ++++------ 14 files changed, 933 insertions(+), 360 deletions(-) diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index ce04290..b7df29d 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -179,6 +179,17 @@ class AppInitializer { firebaseService: firebaseService, nostrService: nostrService, ); + + // Restore session if one was previously saved + try { + final restored = await sessionService.restoreSession(); + if (restored) { + Logger.info('Previous session restored'); + } + } catch (e) { + Logger.warning('Failed to restore session: $e'); + } + Logger.info('Session service initialized'); // Initialize RecipeService diff --git a/lib/data/blossom/blossom_service.dart b/lib/data/blossom/blossom_service.dart index 0d0d2e3..3c98abd 100644 --- a/lib/data/blossom/blossom_service.dart +++ b/lib/data/blossom/blossom_service.dart @@ -419,5 +419,47 @@ class BlossomService implements MediaServiceInterface { final cacheKey = '${sanitizedHash}_${isThumbnail ? 'thumb' : 'full'}'; return path.join(_imageCacheDirectory!.path, cacheKey); } + + /// Clears the cache for a specific image URL or blob hash. + @override + Future clearImageCache(String imageUrlOrBlobHash) async { + // Extract blob hash from URL if it's a full URL + String blobHash = imageUrlOrBlobHash; + if (imageUrlOrBlobHash.startsWith('http://') || imageUrlOrBlobHash.startsWith('https://')) { + // Extract hash from URL like: https://media.based21.com/08e301596aef7a232dcec85c4725f49ff88864f26a3c572f1fa6d1aedaecaf7e.webp + final uri = Uri.parse(imageUrlOrBlobHash); + final pathSegments = uri.pathSegments; + if (pathSegments.isNotEmpty) { + // Get the last segment (the hash with extension) + final lastSegment = pathSegments.last; + // Remove extension if present + blobHash = lastSegment.split('.').first; + } + } + + // Clear both thumbnail and full image cache + final thumbCachePath = _getCacheFilePath(blobHash, true); + final fullCachePath = _getCacheFilePath(blobHash, false); + + try { + if (thumbCachePath.isNotEmpty) { + final thumbFile = File(thumbCachePath); + if (await thumbFile.exists()) { + await thumbFile.delete(); + Logger.debug('Cleared thumbnail cache: $blobHash'); + } + } + if (fullCachePath.isNotEmpty) { + final fullFile = File(fullCachePath); + if (await fullFile.exists()) { + await fullFile.delete(); + Logger.debug('Cleared full image cache: $blobHash'); + } + } + } catch (e) { + Logger.warning('Failed to clear image cache for $blobHash: $e'); + // Don't throw - cache clearing is best effort + } + } } diff --git a/lib/data/immich/immich_service.dart b/lib/data/immich/immich_service.dart index dee668d..b4a331c 100644 --- a/lib/data/immich/immich_service.dart +++ b/lib/data/immich/immich_service.dart @@ -787,4 +787,46 @@ class ImmichService implements MediaServiceInterface { throw ImmichException('Failed to get server info: $e'); } } + + /// Clears the cache for a specific image URL or asset ID. + @override + Future clearImageCache(String imageUrlOrAssetId) async { + // Extract asset ID from URL if it's a full URL + String assetId = imageUrlOrAssetId; + if (imageUrlOrAssetId.startsWith('http://') || imageUrlOrAssetId.startsWith('https://')) { + // Extract asset ID from Immich URL like: https://immich.example.com/api/assets/{id}/thumbnail + final uri = Uri.parse(imageUrlOrAssetId); + final pathSegments = uri.pathSegments; + if (pathSegments.length >= 3 && pathSegments[0] == 'api' && pathSegments[1] == 'assets') { + assetId = pathSegments[2]; + } else { + // If we can't extract asset ID, try to use the URL as-is + assetId = imageUrlOrAssetId; + } + } + + // Clear both thumbnail and full image cache + final thumbCachePath = _getCacheFilePath(assetId, true); + final fullCachePath = _getCacheFilePath(assetId, false); + + try { + if (thumbCachePath.isNotEmpty) { + final thumbFile = File(thumbCachePath); + if (await thumbFile.exists()) { + await thumbFile.delete(); + Logger.debug('Cleared thumbnail cache: $assetId'); + } + } + if (fullCachePath.isNotEmpty) { + final fullFile = File(fullCachePath); + if (await fullFile.exists()) { + await fullFile.delete(); + Logger.debug('Cleared full image cache: $assetId'); + } + } + } catch (e) { + Logger.warning('Failed to clear image cache for $assetId: $e'); + // Don't throw - cache clearing is best effort + } + } } diff --git a/lib/data/media/media_service_interface.dart b/lib/data/media/media_service_interface.dart index 5aa584c..7c279cf 100644 --- a/lib/data/media/media_service_interface.dart +++ b/lib/data/media/media_service_interface.dart @@ -15,5 +15,9 @@ abstract class MediaServiceInterface { /// Gets the base URL. String get baseUrl; + + /// Clears the cache for a specific image URL or asset ID. + /// This is useful when images are removed from recipes to free up cache space. + Future clearImageCache(String imageUrlOrAssetId); } diff --git a/lib/data/media/multi_media_service.dart b/lib/data/media/multi_media_service.dart index d5e4758..e934e41 100644 --- a/lib/data/media/multi_media_service.dart +++ b/lib/data/media/multi_media_service.dart @@ -274,5 +274,23 @@ class MultiMediaService implements MediaServiceInterface { } return _servers.isNotEmpty ? _servers.first.baseUrl : ''; } + + /// Clears the cache for a specific image URL or asset ID. + /// Tries all configured servers to clear the cache. + @override + Future clearImageCache(String imageUrlOrAssetId) async { + // Try to clear cache from all configured servers + for (final config in _servers) { + try { + final service = _createServiceFromConfig(config); + if (service != null) { + await service.clearImageCache(imageUrlOrAssetId); + } + } catch (e) { + Logger.warning('Failed to clear cache from ${config.type} server ${config.baseUrl}: $e'); + // Continue to next server + } + } + } } diff --git a/lib/data/recipes/recipe_service.dart b/lib/data/recipes/recipe_service.dart index a4a2597..f7d8325 100644 --- a/lib/data/recipes/recipe_service.dart +++ b/lib/data/recipes/recipe_service.dart @@ -244,6 +244,23 @@ class RecipeService { await _ensureInitializedOrReinitialize(); try { + // Try to get NostrKeyPair from SessionService if not already set + if (_nostrKeyPair == null) { + try { + final sessionService = ServiceLocator.instance.sessionService; + if (sessionService != null && sessionService.currentUser != null) { + final user = sessionService.currentUser!; + if (user.nostrPrivateKey != null) { + final keyPair = NostrKeyPair.fromNsec(user.nostrPrivateKey!); + _nostrKeyPair = keyPair; + Logger.info('Retrieved NostrKeyPair from SessionService for recipe creation'); + } + } + } catch (e) { + Logger.warning('Failed to retrieve NostrKeyPair from SessionService: $e'); + } + } + final now = DateTime.now().millisecondsSinceEpoch; final recipeWithTimestamps = recipe.copyWith( createdAt: now, @@ -294,6 +311,26 @@ class RecipeService { Future updateRecipe(RecipeModel recipe) async { _ensureInitialized(); try { + // Try to get NostrKeyPair from SessionService if not already set + if (_nostrKeyPair == null) { + try { + final sessionService = ServiceLocator.instance.sessionService; + if (sessionService != null && sessionService.currentUser != null) { + final user = sessionService.currentUser!; + if (user.nostrPrivateKey != null) { + final keyPair = NostrKeyPair.fromNsec(user.nostrPrivateKey!); + _nostrKeyPair = keyPair; + Logger.info('Retrieved NostrKeyPair from SessionService for recipe update'); + } + } + } catch (e) { + Logger.warning('Failed to retrieve NostrKeyPair from SessionService: $e'); + } + } + + // Get the old recipe to compare image URLs + final oldRecipe = await getRecipe(recipe.id, includeDeleted: false); + final updatedRecipe = recipe.copyWith( updatedAt: DateTime.now().millisecondsSinceEpoch, ); @@ -311,14 +348,45 @@ class RecipeService { Logger.info('Recipe updated: ${recipe.id}'); + // Clear cache for removed images + if (oldRecipe != null) { + final removedImages = oldRecipe.imageUrls + .where((url) => !updatedRecipe.imageUrls.contains(url)) + .toList(); + + if (removedImages.isNotEmpty) { + final mediaService = ServiceLocator.instance.mediaService; + if (mediaService != null) { + for (final imageUrl in removedImages) { + try { + await mediaService.clearImageCache(imageUrl); + Logger.debug('Cleared cache for removed image: $imageUrl'); + } catch (e) { + Logger.warning('Failed to clear cache for image $imageUrl: $e'); + // Don't fail the update if cache clearing fails + } + } + } + } + } + // Publish to Nostr if available if (_nostrService != null && _nostrKeyPair != null) { try { + Logger.info('Publishing recipe update ${recipe.id} to Nostr...'); await _publishRecipeToNostr(updatedRecipe); + Logger.info('Recipe update ${recipe.id} published to Nostr successfully'); } catch (e) { Logger.warning('Failed to publish recipe update to Nostr: $e'); // Don't fail the update if Nostr publish fails } + } else { + if (_nostrService == null) { + Logger.warning('NostrService not available, skipping recipe update publish'); + } + if (_nostrKeyPair == null) { + Logger.warning('Nostr keypair not set, skipping recipe update publish. Call setNostrKeyPair() first.'); + } } return updatedRecipe; @@ -902,9 +970,24 @@ class RecipeService { Logger.debug('Stored recipe ${recipe.id} (${recipe.title}) in database'); storedCount++; } else { - // Update existing recipe (might have newer data from Nostr) - await updateRecipe(recipe); - storedCount++; + // Only update if the Nostr version is newer than the local version + // This prevents overwriting local updates with stale data from relays + final localUpdatedAt = existing.updatedAt; + final remoteUpdatedAt = recipe.updatedAt; + + if (remoteUpdatedAt > localUpdatedAt) { + // Remote is newer, update local + await updateRecipe(recipe); + Logger.debug('Updated recipe ${recipe.id} from Nostr (remote: $remoteUpdatedAt > local: $localUpdatedAt)'); + storedCount++; + } else if (remoteUpdatedAt < localUpdatedAt) { + // Local is newer, skip update to preserve local changes + Logger.debug('Skipped updating recipe ${recipe.id} from Nostr (local: $localUpdatedAt > remote: $remoteUpdatedAt)'); + } else { + // Same timestamp, update anyway (might have other changes) + await updateRecipe(recipe); + storedCount++; + } } } catch (e) { Logger.warning('Failed to store recipe ${recipe.id}: $e'); @@ -1099,6 +1182,12 @@ class RecipeService { /// Publishes a recipe to Nostr as a kind 30000 event (NIP-33 parameterized replaceable event). Future _publishRecipeToNostr(RecipeModel recipe) async { if (_nostrService == null || _nostrKeyPair == null) { + if (_nostrService == null) { + Logger.warning('_publishRecipeToNostr: NostrService is null, cannot publish recipe ${recipe.id}'); + } + if (_nostrKeyPair == null) { + Logger.warning('_publishRecipeToNostr: NostrKeyPair is null, cannot publish recipe ${recipe.id}'); + } return; } @@ -1147,11 +1236,23 @@ class RecipeService { ); // Publish to all enabled relays + final enabledRelays = _nostrService!.getRelays().where((r) => r.isEnabled).toList(); + Logger.info('Publishing recipe ${recipe.id} to ${enabledRelays.length} enabled relay(s)'); + + if (enabledRelays.isEmpty) { + Logger.warning('No enabled relays available for publishing recipe ${recipe.id}'); + } + final results = await _nostrService!.publishEventToAllRelays(event); // Log results final successCount = results.values.where((success) => success).length; - Logger.info('Published recipe ${recipe.id} to $successCount/${results.length} relays'); + final totalRelays = results.length; + Logger.info('Published recipe ${recipe.id} to $successCount/$totalRelays relay(s)'); + + if (successCount == 0 && totalRelays > 0) { + Logger.warning('Failed to publish recipe ${recipe.id} to any relay. Results: $results'); + } // Update recipe with Nostr event ID final updatedRecipe = recipe.copyWith(nostrEventId: event.id); diff --git a/lib/data/session/session_service.dart b/lib/data/session/session_service.dart index 9ff3fcf..e870549 100644 --- a/lib/data/session/session_service.dart +++ b/lib/data/session/session_service.dart @@ -123,6 +123,9 @@ class SessionService { // Set as current user _currentUser = user; + // Persist session + await _saveSession(); + return user; } catch (e) { throw SessionException('Failed to login: $e'); @@ -228,6 +231,9 @@ class SessionService { // Set as current user _currentUser = user; + // Persist session + await _saveSession(); + // Load preferred relays from NIP-05 if available await _loadPreferredRelaysIfAvailable(); @@ -314,6 +320,9 @@ class SessionService { // Clear current user _currentUser = null; + // Clear persisted session + await _clearSession(); + // Clean up user storage references _userDbPaths.remove(userId); _userCacheDirs.remove(userId); @@ -678,5 +687,124 @@ class SessionService { Logger.warning('Failed to load preferred relays from NIP-05: $e'); } } + + /// Saves the current session to persistent storage. + Future _saveSession() async { + if (_currentUser == null) return; + + try { + final sessionData = _currentUser!.toMap(); + final item = Item( + id: 'current_session', + data: sessionData, + ); + + // Check if session already exists + final existing = await _localStorage.getItem('current_session'); + if (existing != null) { + await _localStorage.updateItem(item); + } else { + await _localStorage.insertItem(item); + } + + Logger.debug('Session saved to persistent storage'); + } catch (e) { + Logger.warning('Failed to save session: $e'); + // Don't throw - session persistence is not critical + } + } + + /// Clears the persisted session from storage. + Future _clearSession() async { + try { + await _localStorage.deleteItem('current_session'); + Logger.debug('Session cleared from persistent storage'); + } catch (e) { + Logger.warning('Failed to clear session: $e'); + // Don't throw - clearing session is not critical + } + } + + /// Restores the session from persistent storage. + /// + /// This should be called during app initialization to restore the user session + /// if one was previously saved. + /// + /// Returns true if a session was restored, false otherwise. + Future restoreSession() async { + if (_currentUser != null) { + Logger.debug('Session already active, skipping restore'); + return true; + } + + try { + final sessionItem = await _localStorage.getItem('current_session'); + if (sessionItem == null || sessionItem.data.isEmpty) { + Logger.debug('No persisted session found'); + return false; + } + + // Restore user from stored data + final user = User.fromMap(sessionItem.data); + + // If it's a Nostr login, we need to restore the Nostr keypair + if (user.nostrPrivateKey != null && _nostrService != null) { + try { + // Parse the Nostr keypair + final keyPair = NostrKeyPair.fromNsec(user.nostrPrivateKey!); + + // Set Nostr keypair on RecipeService + final recipeService = ServiceLocator.instance.recipeService; + if (recipeService != null) { + recipeService.setNostrKeyPair(keyPair); + Logger.info('Nostr keypair restored on RecipeService'); + } + + // Set Nostr keypair on BlossomService if applicable + final mediaService = ServiceLocator.instance.mediaService; + if (mediaService != null) { + if (mediaService is BlossomService) { + mediaService.setNostrKeyPair(keyPair); + Logger.info('Nostr keypair restored on BlossomService'); + } else { + try { + (mediaService as dynamic).setNostrKeyPair(keyPair); + Logger.info('Nostr keypair restored on media service (dynamic)'); + } catch (_) { + Logger.debug('Media service does not support Nostr keypair: ${mediaService.runtimeType}'); + } + } + } + } catch (e) { + Logger.warning('Failed to restore Nostr keypair: $e'); + // Continue with restore even if keypair restoration fails + } + } + + // Create user-specific storage paths + await _setupUserStorage(user); + + // Set as current user + _currentUser = user; + + Logger.info('Session restored for user: ${user.username} (${user.id.substring(0, 16)}...)'); + + // Load preferred relays from NIP-05 if available (async, don't wait) + _loadPreferredRelaysIfAvailable().catchError((e) { + Logger.warning('Failed to load preferred relays after restore: $e'); + }); + + return true; + } catch (e) { + Logger.warning('Failed to restore session: $e'); + // Clear corrupted session data + try { + await _clearSession(); + } catch (_) { + // Ignore errors when clearing + } + return false; + } + } } diff --git a/lib/ui/favourites/favourites_screen.dart b/lib/ui/favourites/favourites_screen.dart index d91a565..d885662 100644 --- a/lib/ui/favourites/favourites_screen.dart +++ b/lib/ui/favourites/favourites_screen.dart @@ -22,6 +22,7 @@ class _FavouritesScreenState extends State { RecipeService? _recipeService; bool _wasLoggedIn = false; bool _isMinimalView = false; + bool _hasChanges = false; // Track if any changes were made @override void initState() { @@ -138,6 +139,7 @@ class _FavouritesScreenState extends State { try { await _recipeService!.deleteRecipe(recipe.id); if (mounted) { + _hasChanges = true; // Mark that changes were made ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Recipe deleted successfully'), @@ -190,6 +192,7 @@ class _FavouritesScreenState extends State { final updatedRecipe = recipe.copyWith(isFavourite: !recipe.isFavourite); await _recipeService!.updateRecipe(updatedRecipe); if (mounted) { + _hasChanges = true; // Mark that changes were made _loadFavourites(); } } catch (e) { @@ -207,23 +210,32 @@ class _FavouritesScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Favourites'), - elevation: 0, - actions: [ - // View mode toggle icon - IconButton( - icon: Icon(_isMinimalView ? Icons.grid_view : Icons.view_list), - onPressed: () { - setState(() { - _isMinimalView = !_isMinimalView; - }); - }, - ), - ], + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + // When navigating back, return true if changes were made + if (!didPop) { + Navigator.of(context).pop(_hasChanges); + } + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Favourites'), + elevation: 0, + actions: [ + // View mode toggle icon + IconButton( + icon: Icon(_isMinimalView ? Icons.grid_view : Icons.view_list), + onPressed: () { + setState(() { + _isMinimalView = !_isMinimalView; + }); + }, + ), + ], + ), + body: _buildBody(), ), - body: _buildBody(), ); } diff --git a/lib/ui/navigation/main_navigation_scaffold.dart b/lib/ui/navigation/main_navigation_scaffold.dart index f1bf5a1..f557e42 100644 --- a/lib/ui/navigation/main_navigation_scaffold.dart +++ b/lib/ui/navigation/main_navigation_scaffold.dart @@ -19,7 +19,7 @@ class MainNavigationScaffold extends StatefulWidget { } class MainNavigationScaffoldState extends State { - int _currentIndex = 0; + late int _currentIndex; bool _wasLoggedIn = false; bool get _isLoggedIn => ServiceLocator.instance.sessionService?.isLoggedIn ?? false; @@ -27,7 +27,10 @@ class MainNavigationScaffoldState extends State { @override void initState() { super.initState(); - _wasLoggedIn = _isLoggedIn; + final isLoggedIn = _isLoggedIn; + _wasLoggedIn = isLoggedIn; + // Show login screen (index 3) if not logged in, otherwise show Home (index 0) + _currentIndex = isLoggedIn ? 0 : 3; } @override @@ -95,20 +98,24 @@ class MainNavigationScaffoldState extends State { } /// Public method to navigate to Favourites screen (used from Recipes AppBar). - void navigateToFavourites() { + /// Returns true if any changes were made that require refreshing the Recipes list. + Future navigateToFavourites() async { final isLoggedIn = _isLoggedIn; if (!isLoggedIn) { // Redirect to User/Session tab to prompt login navigateToUser(); - return; + return false; } - // Navigate to Favourites as a full-screen route - Navigator.push( + // Navigate to Favourites as a full-screen route and await result + // FavouritesScreen will return true if any changes were made + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => const FavouritesScreen(), ), ); + // Return true if changes were made (to trigger refresh in RecipesScreen) + return result ?? false; } void _onAddRecipePressed(BuildContext context) async { @@ -180,7 +187,18 @@ class MainNavigationScaffoldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { - _currentIndex = 0; // Switch to Home if trying to access protected tabs + _currentIndex = 3; // Switch to Session/Login screen if trying to access protected tabs + }); + } + }); + } + + // If logged out and on Home screen, redirect to login + if (!isLoggedIn && _currentIndex == 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _currentIndex = 3; // Switch to Session/Login screen }); } }); diff --git a/lib/ui/recipes/recipes_screen.dart b/lib/ui/recipes/recipes_screen.dart index e629935..18873a6 100644 --- a/lib/ui/recipes/recipes_screen.dart +++ b/lib/ui/recipes/recipes_screen.dart @@ -27,7 +27,7 @@ class RecipesScreen extends StatefulWidget { } } -class _RecipesScreenState extends State { +class _RecipesScreenState extends State with WidgetsBindingObserver { List _recipes = []; List _filteredRecipes = []; bool _isLoading = false; @@ -37,10 +37,12 @@ class _RecipesScreenState extends State { bool _isMinimalView = false; final TextEditingController _searchController = TextEditingController(); bool _isSearching = false; + DateTime? _lastRefreshTime; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _initializeService(); _checkLoginState(); _loadPreferences(); @@ -49,10 +51,20 @@ class _RecipesScreenState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _searchController.dispose(); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + // Refresh recipes when app resumes to ensure UI is up-to-date + if (state == AppLifecycleState.resumed && _recipeService != null) { + _refreshIfNeeded(); + } + } + Future _loadPreferences() async { try { final localStorage = ServiceLocator.instance.localStorageService; @@ -111,7 +123,20 @@ class _RecipesScreenState extends State { super.didChangeDependencies(); _checkLoginState(); if (_recipeService != null) { - _loadRecipes(fetchFromNostr: false); + // Refresh when dependencies change (e.g., returning from another screen) + _refreshIfNeeded(); + } + } + + /// Refreshes recipes if enough time has passed since last refresh. + /// This prevents excessive refreshes while ensuring UI stays up-to-date. + void _refreshIfNeeded() { + final now = DateTime.now(); + // Refresh if never refreshed, or if more than 1 second has passed + if (_lastRefreshTime == null || + now.difference(_lastRefreshTime!).inSeconds > 1) { + _lastRefreshTime = now; + _loadRecipes(fetchFromNostr: false, showLoading: false); } } @@ -165,15 +190,42 @@ class _RecipesScreenState extends State { } } - Future _loadRecipes({bool fetchFromNostr = false}) async { + Future _loadRecipes({bool fetchFromNostr = false, bool clearImageCache = false, bool showLoading = true}) async { if (_recipeService == null) return; - setState(() { - _isLoading = true; - _errorMessage = null; - }); + if (showLoading) { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + } try { + // Clear image cache if requested (e.g., on pull-to-refresh) + if (clearImageCache) { + final mediaService = ServiceLocator.instance.mediaService; + if (mediaService != null) { + try { + // Clear cache for all recipe images + final recipes = await _recipeService!.getAllRecipes(); + for (final recipe in recipes) { + for (final imageUrl in recipe.imageUrls) { + try { + await mediaService.clearImageCache(imageUrl); + } catch (e) { + // Ignore individual cache clear failures + Logger.debug('Failed to clear cache for $imageUrl: $e'); + } + } + } + Logger.info('Cleared image cache for ${recipes.length} recipe(s)'); + } catch (e) { + Logger.warning('Failed to clear image cache: $e'); + // Don't fail the load if cache clearing fails + } + } + } + if (fetchFromNostr) { final sessionService = ServiceLocator.instance.sessionService; if (sessionService != null && sessionService.currentUser != null) { @@ -196,7 +248,9 @@ class _RecipesScreenState extends State { setState(() { _recipes = recipes; _filteredRecipes = List.from(recipes); - _isLoading = false; + if (showLoading) { + _isLoading = false; + } }); _filterRecipes(); // Apply current search filter } @@ -217,8 +271,12 @@ class _RecipesScreenState extends State { builder: (context) => AddRecipeScreen(recipe: recipe, viewMode: true), ), ); + // Always reload recipes after viewing/editing to ensure UI is up-to-date + // Don't fetch from Nostr immediately after editing - relays may not have synced yet + // The local database already has the updated recipe, so just reload from local + // Skip loading indicator for instant UI update if (mounted) { - _loadRecipes(); + await _loadRecipes(fetchFromNostr: false, showLoading: false); } } @@ -315,10 +373,14 @@ class _RecipesScreenState extends State { ), IconButton( icon: const Icon(Icons.favorite), - onPressed: () { - // Navigate to Favourites screen + onPressed: () async { + // Navigate to Favourites screen and refresh if changes were made final scaffold = context.findAncestorStateOfType(); - scaffold?.navigateToFavourites(); + final hasChanges = await scaffold?.navigateToFavourites() ?? false; + // Refresh recipes list if any changes were made in Favourites screen + if (hasChanges && mounted && _recipeService != null) { + await _loadRecipes(fetchFromNostr: false, showLoading: false); + } }, tooltip: 'Favourites', ), @@ -394,7 +456,7 @@ class _RecipesScreenState extends State { } return RefreshIndicator( - onRefresh: () => _loadRecipes(fetchFromNostr: true), + onRefresh: () => _loadRecipes(fetchFromNostr: true, clearImageCache: true), child: _filteredRecipes.isEmpty ? SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/ui/relay_management/relay_management_screen.dart b/lib/ui/relay_management/relay_management_screen.dart index 8206d88..cc44761 100644 --- a/lib/ui/relay_management/relay_management_screen.dart +++ b/lib/ui/relay_management/relay_management_screen.dart @@ -37,7 +37,8 @@ class _RelayManagementScreenState extends State { MultiMediaService? _multiMediaService; List _mediaServers = []; MediaServerConfig? _defaultMediaServer; - bool _mediaServersExpanded = false; // Track expansion state + bool _mediaServersExpanded = false; + bool _settingsExpanded = true; // Settings expanded by default // Store original values to detect changes List _originalMediaServers = []; @@ -504,180 +505,165 @@ class _RelayManagementScreenState extends State { return SingleChildScrollView( child: Column( children: [ - // Settings section - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Settings', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ElevatedButton.icon( - onPressed: _hasUnsavedChanges ? _saveAllSettings : null, - icon: const Icon(Icons.save, size: 18), - label: const Text('Save'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ], - ), - if (_hasUnsavedChanges) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'You have unsaved changes', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.orange, - fontStyle: FontStyle.italic, - ), + // Settings section - Now using ExpansionTile for uniformity + ExpansionTile( + title: const Text('Settings'), + subtitle: _hasUnsavedChanges + ? const Text('You have unsaved changes', style: TextStyle(color: Colors.orange)) + : Text('${_useNip05RelaysAutomatically ? "NIP-05" : "Manual"} relays • ${_isDarkMode ? "Dark" : "Light"} mode'), + initiallyExpanded: _settingsExpanded, + trailing: _hasUnsavedChanges + ? ElevatedButton.icon( + onPressed: _saveAllSettings, + icon: const Icon(Icons.save, size: 18), + label: const Text('Save'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - ), - const SizedBox(height: 12), - if (_isLoadingSetting) - const Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - SizedBox(width: 12), - Text('Loading settings...'), - ], ) - else - SwitchListTile( - title: const Text('Use NIP-05 relays automatically'), - subtitle: const Text( - 'Automatically replace relays with NIP-05 preferred relays upon login', - style: TextStyle(fontSize: 12), - ), - value: _useNip05RelaysAutomatically, - onChanged: (value) { - _saveSetting('use_nip05_relays_automatically', value); - }, - contentPadding: EdgeInsets.zero, - ), - const SizedBox(height: 8), - SwitchListTile( - title: const Text('Dark Mode'), - subtitle: const Text( - 'Enable dark theme for the app', - style: TextStyle(fontSize: 12), - ), - value: _isDarkMode, - onChanged: (value) { - _saveSetting('dark_mode', value); - }, - contentPadding: EdgeInsets.zero, - ), - const SizedBox(height: 16), - // Media Server Settings - Multiple Servers - Builder( - builder: (context) { - final immichEnabled = dotenv.env['IMMICH_ENABLE']?.toLowerCase() != 'false'; - final defaultBlossomServer = dotenv.env['BLOSSOM_SERVER'] ?? 'https://media.based21.com'; - - return ExpansionTile( - key: ValueKey('media-servers-${_mediaServers.length}-${_mediaServersExpanded}'), - title: const Text('Media Servers'), - subtitle: Text(_mediaServers.isEmpty - ? 'No servers configured' - : '${_mediaServers.length} server(s)${_defaultMediaServer != null ? " • Default: ${_defaultMediaServer!.name ?? _defaultMediaServer!.type}" : ""}'), - initiallyExpanded: _mediaServersExpanded, - onExpansionChanged: (expanded) { - setState(() { - _mediaServersExpanded = expanded; - }); - }, + : null, + onExpansionChanged: (expanded) { + setState(() { + _settingsExpanded = expanded; + }); + }, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_isLoadingSetting) + const Row( children: [ - // Server list - if (_mediaServers.isEmpty) - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'No media servers configured. Add one to get started.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ) - else - ...List.generate(_mediaServers.length, (index) { - final server = _mediaServers[index]; - return ListTile( - key: ValueKey(server.id), - leading: Icon( - server.isDefault ? Icons.star : Icons.star_border, - color: server.isDefault ? Colors.amber : Colors.grey, - ), - title: Text(server.name ?? '${server.type.toUpperCase()} Server'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(server.baseUrl), - if (server.isDefault) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Chip( - label: const Text('Default'), - labelStyle: const TextStyle(fontSize: 10), - padding: EdgeInsets.zero, - ), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => _editMediaServer(server), + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 12), + Text('Loading settings...'), + ], + ) + else ...[ + SwitchListTile( + title: const Text('Use NIP-05 relays automatically'), + subtitle: const Text( + 'Automatically replace relays with NIP-05 preferred relays upon login', + style: TextStyle(fontSize: 12), + ), + value: _useNip05RelaysAutomatically, + onChanged: (value) { + _saveSetting('use_nip05_relays_automatically', value); + }, + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 8), + SwitchListTile( + title: const Text('Dark Mode'), + subtitle: const Text( + 'Enable dark theme for the app', + style: TextStyle(fontSize: 12), + ), + value: _isDarkMode, + onChanged: (value) { + _saveSetting('dark_mode', value); + }, + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 16), + // Media Server Settings - Multiple Servers + Builder( + builder: (context) { + final immichEnabled = dotenv.env['IMMICH_ENABLE']?.toLowerCase() != 'false'; + final defaultBlossomServer = dotenv.env['BLOSSOM_SERVER'] ?? 'https://media.based21.com'; + + return ExpansionTile( + key: ValueKey('media-servers-${_mediaServers.length}-${_mediaServersExpanded}'), + title: const Text('Media Servers'), + subtitle: Text(_mediaServers.isEmpty + ? 'No servers configured' + : '${_mediaServers.length} server(s)${_defaultMediaServer != null ? " • Default: ${_defaultMediaServer!.name ?? _defaultMediaServer!.type}" : ""}'), + initiallyExpanded: _mediaServersExpanded, + onExpansionChanged: (expanded) { + setState(() { + _mediaServersExpanded = expanded; + }); + }, + children: [ + // Server list + if (_mediaServers.isEmpty) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No media servers configured. Add one to get started.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _removeMediaServer(server), + ), + ) + else + ...List.generate(_mediaServers.length, (index) { + final server = _mediaServers[index]; + return ListTile( + key: ValueKey(server.id), + leading: Icon( + server.isDefault ? Icons.star : Icons.star_border, + color: server.isDefault ? Colors.amber : Colors.grey, + ), + title: Text(server.name ?? '${server.type.toUpperCase()} Server'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(server.baseUrl), + if (server.isDefault) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Chip( + label: const Text('Default'), + labelStyle: const TextStyle(fontSize: 10), + padding: EdgeInsets.zero, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _editMediaServer(server), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _removeMediaServer(server), + ), + ], ), - ], + onTap: () => _setDefaultServer(server), + ); + }), + const Divider(), + // Add server button + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + onPressed: () => _addMediaServer(immichEnabled, defaultBlossomServer), + icon: const Icon(Icons.add), + label: const Text('Add Media Server'), ), - onTap: () => _setDefaultServer(server), - ); - }), - const Divider(), - // Add server button - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( - onPressed: () => _addMediaServer(immichEnabled, defaultBlossomServer), - icon: const Icon(Icons.add), - label: const Text('Add Media Server'), - ), - ), - ], - ); - }, - ), - ], - ), + ), + ], + ); + }, + ), + ], + ], + ), + ), + ], ), // Error message if (widget.controller.error != null) diff --git a/lib/ui/session/session_screen.dart b/lib/ui/session/session_screen.dart index dca2cfb..beaaa2f 100644 --- a/lib/ui/session/session_screen.dart +++ b/lib/ui/session/session_screen.dart @@ -31,6 +31,10 @@ class _SessionScreenState extends State { bool _useFirebaseAuth = false; bool _useNostrLogin = false; NostrKeyPair? _generatedKeyPair; + Uint8List? _avatarBytes; + bool _isLoadingAvatar = false; + String? _lastProfilePictureUrl; // Track URL to avoid reloading + DateTime? _avatarLastLoaded; // Track when avatar was last loaded @override void initState() { @@ -38,6 +42,30 @@ class _SessionScreenState extends State { // Check if Firebase Auth is available _useFirebaseAuth = ServiceLocator.instance.firebaseService?.isEnabled == true && ServiceLocator.instance.firebaseService?.config.authEnabled == true; + _loadAvatar(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Load avatar if not already loaded or if URL changed + final sessionService = ServiceLocator.instance.sessionService; + if (sessionService != null && sessionService.isLoggedIn) { + final currentUser = sessionService.currentUser; + final profilePictureUrl = currentUser?.nostrProfile?.picture; + // Load if URL changed or if we don't have cached bytes yet + if (profilePictureUrl != _lastProfilePictureUrl || + (profilePictureUrl != null && _avatarBytes == null && !_isLoadingAvatar)) { + _loadAvatar(); + } + } else if (_avatarBytes != null) { + // Clear avatar if logged out + setState(() { + _avatarBytes = null; + _lastProfilePictureUrl = null; + _avatarLastLoaded = null; + }); + } } @override @@ -96,6 +124,7 @@ class _SessionScreenState extends State { ), ); setState(() {}); + _loadAvatar(); // Reload avatar after login widget.onSessionChanged?.call(); } return; @@ -138,6 +167,7 @@ class _SessionScreenState extends State { ), ); setState(() {}); + _loadAvatar(); // Reload avatar after login // Notify parent that session state changed widget.onSessionChanged?.call(); } @@ -194,6 +224,7 @@ class _SessionScreenState extends State { ), ); setState(() {}); + _loadAvatar(); // Reload avatar after login // Notify parent that session state changed widget.onSessionChanged?.call(); } @@ -243,7 +274,10 @@ class _SessionScreenState extends State { content: Text('Logout successful'), ), ); - setState(() {}); + setState(() { + _avatarBytes = null; // Clear avatar on logout + _lastProfilePictureUrl = null; + }); // Notify parent that session state changed widget.onSessionChanged?.call(); } @@ -265,7 +299,132 @@ class _SessionScreenState extends State { } } - /// Builds a profile picture widget using ImmichService for authenticated access. + /// Loads the avatar image and caches it in state. + Future _loadAvatar() async { + final sessionService = ServiceLocator.instance.sessionService; + if (sessionService == null || !sessionService.isLoggedIn) { + // Clear avatar when logged out + if (mounted) { + setState(() { + _avatarBytes = null; + _isLoadingAvatar = false; + _lastProfilePictureUrl = null; + }); + } + return; + } + + final currentUser = sessionService.currentUser; + if (currentUser == null) { + if (mounted) { + setState(() { + _avatarBytes = null; + _isLoadingAvatar = false; + _lastProfilePictureUrl = null; + }); + } + return; + } + + final profilePictureUrl = currentUser.nostrProfile?.picture; + if (profilePictureUrl == null || profilePictureUrl.isEmpty) { + // Clear avatar if no profile picture + if (mounted) { + setState(() { + _avatarBytes = null; + _isLoadingAvatar = false; + _lastProfilePictureUrl = null; + }); + } + return; + } + + // Don't reload if it's the same URL and we already have the bytes in memory + // Only reload if it's been more than 5 minutes since last load + // Note: MediaService handles disk caching, so we can rely on it for persistence + final shouldReload = profilePictureUrl != _lastProfilePictureUrl || + _avatarBytes == null || + (_avatarLastLoaded != null && + DateTime.now().difference(_avatarLastLoaded!).inMinutes > 5); + + if (!shouldReload && _avatarBytes != null) { + return; + } + + // Update last URL before loading (even if it fails, we don't want to retry immediately) + _lastProfilePictureUrl = profilePictureUrl; + + if (mounted) { + setState(() { + _isLoadingAvatar = true; + }); + } + + try { + // Extract asset ID from Immich URL using regex + final mediaService = ServiceLocator.instance.mediaService; + if (mediaService != null) { + // Try to extract asset ID from URL (format: .../api/assets/{id}/original) + final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(profilePictureUrl); + String? assetId; + + if (assetIdMatch != null) { + assetId = assetIdMatch.group(1); + } else { + // For Blossom URLs, use the full URL + assetId = profilePictureUrl; + } + + if (assetId != null) { + // Use MediaService which has built-in disk caching + // MediaService will return cached bytes if available, otherwise fetch and cache + try { + final bytes = await mediaService.fetchImageBytes(assetId, isThumbnail: true); + if (mounted && bytes != null && bytes.isNotEmpty) { + setState(() { + _avatarBytes = bytes; + _isLoadingAvatar = false; + _avatarLastLoaded = DateTime.now(); + }); + return; + } + } catch (e) { + // If fetch fails (e.g., 404), log but don't mark URL as loaded + // This allows retry on next refresh if the URL becomes available + Logger.warning('Failed to fetch avatar from MediaService: $e'); + if (mounted) { + setState(() { + _isLoadingAvatar = false; + _avatarBytes = null; + // Don't update _lastProfilePictureUrl on error - allows retry + // Only update if we successfully loaded + }); + } + return; // Return early to avoid showing placeholder immediately + } + } + } + + // Fallback: try to fetch as regular image (for non-Immich URLs or shared links) + // For now, we'll just set loading to false + if (mounted) { + setState(() { + _isLoadingAvatar = false; + }); + } + } catch (e) { + // Log error for debugging + Logger.warning('Failed to load avatar: $e'); + if (mounted) { + setState(() { + _isLoadingAvatar = false; + _avatarBytes = null; // Clear on error + }); + } + } + } + + /// Builds a profile picture widget using cached avatar bytes. Widget _buildProfilePicture(String? imageUrl, {double radius = 30}) { if (imageUrl == null) { return CircleAvatar( @@ -274,60 +433,34 @@ class _SessionScreenState extends State { ); } - final mediaService = ServiceLocator.instance.mediaService; - if (mediaService == null) { + if (_isLoadingAvatar) { return CircleAvatar( radius: radius, backgroundColor: Colors.grey[300], - child: const Icon(Icons.person, size: 50), + child: const CircularProgressIndicator(), ); } - - // Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original) - final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); - String? assetId; - - if (assetIdMatch != null) { - assetId = assetIdMatch.group(1); - } else { - // For Blossom URLs, use the full URL - assetId = imageUrl; - } - - if (assetId != null) { - // Use MediaService to fetch image with proper authentication - return FutureBuilder( - future: mediaService.fetchImageBytes(assetId, isThumbnail: true), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return CircleAvatar( - radius: radius, - backgroundColor: Colors.grey[300], - child: const CircularProgressIndicator(), - ); - } - if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { - return CircleAvatar( - radius: radius, - backgroundColor: Colors.grey[300], - child: const Icon(Icons.broken_image), - ); - } - - return CircleAvatar( - radius: radius, - backgroundImage: MemoryImage(snapshot.data!), - ); - }, - ); + if (_avatarBytes != null) { + return CircleAvatar( + radius: radius, + backgroundImage: MemoryImage(_avatarBytes!), + onBackgroundImageError: (_, __) { + // Clear avatar on error + if (mounted) { + setState(() { + _avatarBytes = null; + }); + } + }, + ); } - - // Fallback to direct network image if not an Immich URL or service unavailable + + // Fallback: show placeholder if no cached bytes return CircleAvatar( radius: radius, - backgroundImage: NetworkImage(imageUrl), - onBackgroundImageError: (_, __) {}, + backgroundColor: Colors.grey[300], + child: const Icon(Icons.person, size: 50), ); } @@ -342,6 +475,7 @@ class _SessionScreenState extends State { // If profile was updated, refresh the screen if (result == true && mounted) { setState(() {}); + _loadAvatar(); // Reload avatar after profile update // Also trigger the session changed callback to update parent widget.onSessionChanged?.call(); } @@ -351,9 +485,36 @@ class _SessionScreenState extends State { if (ServiceLocator.instance.sessionService == null) return; try { - await ServiceLocator.instance.sessionService!.refreshNostrProfile(); + // Store the old profile picture URL to detect changes + final sessionService = ServiceLocator.instance.sessionService!; + final oldProfilePictureUrl = sessionService.currentUser?.nostrProfile?.picture; + + await sessionService.refreshNostrProfile(); + if (mounted) { - setState(() {}); + // Wait for relays to connect and profile to fully sync + // The profile refresh triggers relay reconnection which takes time + await Future.delayed(const Duration(milliseconds: 800)); + + // Get the new profile picture URL after refresh + final newProfilePictureUrl = sessionService.currentUser?.nostrProfile?.picture; + + // Clear in-memory cache to force reload from MediaService disk cache or network + setState(() { + _avatarBytes = null; + _avatarLastLoaded = null; + // Only clear last URL if it actually changed, otherwise keep it to allow cache lookup + if (oldProfilePictureUrl != newProfilePictureUrl) { + _lastProfilePictureUrl = null; + } + }); + + // Only reload avatar if we have a valid URL + if (newProfilePictureUrl != null && newProfilePictureUrl.isNotEmpty) { + // Reload avatar after profile refresh - MediaService will use disk cache if available + await _loadAvatar(); + } + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Session data refreshed'), @@ -550,6 +711,18 @@ class _SessionScreenState extends State { ), ], const SizedBox(height: 24), + // No Nostr account text + if (_useNostrLogin) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + 'No Nostr account?', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ), // Login method selector SegmentedButton( segments: const [ @@ -583,7 +756,7 @@ class _SessionScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( - 'Generate Key Pair', + 'Generate a Key pair', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c9de345..b16bc23 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,7 +18,7 @@ import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) diff --git a/pubspec.lock b/pubspec.lock index e58fb6a..3496b1e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "91.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" url: "https://pub.dev" source: hosted - version: "1.3.59" + version: "1.3.41" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "8.4.1" args: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: build - sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d + sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.2" build_config: dependency: transitive description: @@ -133,34 +133,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 - url: "https://pub.dev" - source: hosted - version: "2.7.1" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" + sha256: "04f69b1502f66e22ae7990bbd01eb552b7f12793c4d3ea6e715d0ac5e98bcdac" url: "https://pub.dev" source: hosted - version: "9.3.1" + version: "2.10.2" built_collection: dependency: transitive description: @@ -213,26 +197,26 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c" + sha256: "0af4f1e0b7f82a2082ad2c886b042595d85c7641616688609dd999d33aeef850" url: "https://pub.dev" source: hosted - version: "5.6.12" + version: "5.4.0" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b" + sha256: "6e82bcaf2b8e33f312dfc7c5e5855376fee538467dd6e58dc41bd13996ff5d64" url: "https://pub.dev" source: hosted - version: "6.6.12" + version: "6.4.0" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf + sha256: "38aec6ed732980dea6eec192a25fb55665137edc5c356603d8dc26ff5971f4d8" url: "https://pub.dev" source: hosted - version: "4.4.12" + version: "4.2.0" code_builder: dependency: transitive description: @@ -285,10 +269,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" diff_match_patch: dependency: transitive description: @@ -357,10 +341,10 @@ packages: dependency: transitive description: name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.7.0" file_selector_windows: dependency: transitive description: @@ -373,122 +357,122 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "4f85b161772e1d54a66893ef131c0a44bd9e552efa78b33d5f4f60d2caa5c8a3" + sha256: "7e032ade38dec2a92f543ba02c5f72f54ffaa095c60d2132b867eab56de3bc73" url: "https://pub.dev" source: hosted - version: "11.6.0" + version: "11.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: a44b6d1155ed5cae7641e3de7163111cfd9f6f6c954ca916dc6a3bdfa86bf845 + sha256: b62a2444767d95067a7e36b1d6e335e0b877968574bbbfb656168c46f2e95a13 url: "https://pub.dev" source: hosted - version: "4.4.3" + version: "4.2.2" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: c7d1ed1f86ae64215757518af5576ff88341c8ce5741988c05cc3b2e07b0b273 + sha256: bad44f71f96cfca6c16c9dd4f70b85f123ddca7d5dd698977449fadf298b1782 url: "https://pub.dev" source: hosted - version: "0.5.10+16" + version: "0.5.9+2" firebase_auth: dependency: "direct main" description: name: firebase_auth - sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" + sha256: "6f5792bdc208416bfdfbfe3363b78ce01667b6ebc4c5cb47cfa891f2fca45ab7" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.2.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" + sha256: "80237bb8a92bb0a5e3b40de1c8dbc80254e49ac9e3907b4b47b8e95ac3dd3fad" url: "https://pub.dev" source: hosted - version: "7.7.3" + version: "7.4.4" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e + sha256: "9d315491a6be65ea83511cb0e078544a309c39dd54c0ee355c51dbd6d8c03cc8" url: "https://pub.dev" source: hosted - version: "5.15.3" + version: "5.12.6" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" url: "https://pub.dev" source: hosted - version: "3.15.2" + version: "3.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" url: "https://pub.dev" source: hosted - version: "2.24.1" + version: "2.17.5" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" + sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453" url: "https://pub.dev" source: hosted - version: "15.2.10" + version: "15.1.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" + sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72" url: "https://pub.dev" source: hosted - version: "4.6.10" + version: "4.5.43" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" + sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4" url: "https://pub.dev" source: hosted - version: "3.10.10" + version: "3.8.13" firebase_storage: dependency: "direct main" description: name: firebase_storage - sha256: "958fc88a7ef0b103e694d30beed515c8f9472dde7e8459b029d0e32b8ff03463" + sha256: dfc06d783dbc0b6200a4b936d8cdbd826bd1571c959854d14a70259188d96e85 url: "https://pub.dev" source: hosted - version: "12.4.10" + version: "12.2.0" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface - sha256: d2661c05293c2a940c8ea4bc0444e1b5566c79dd3202c2271140c082c8cd8dd4 + sha256: "3da511301b77514dee5370281923fbbc6d5725c2a0b96004c5c45415e067f234" url: "https://pub.dev" source: hosted - version: "5.2.10" + version: "5.1.28" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: "629a557c5e1ddb97a3666cbf225e97daa0a66335dbbfdfdce113ef9f881e833f" + sha256: "7ad67b1c1c46c995a6bd4f225d240fc9a5fb277fade583631ae38750ffd9be17" url: "https://pub.dev" source: hosted - version: "3.10.17" + version: "3.9.13" fixnum: dependency: transitive description: @@ -564,10 +548,10 @@ packages: dependency: "direct main" description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -588,10 +572,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" image_picker_android: dependency: transitive description: @@ -732,10 +716,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -748,10 +732,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" + sha256: "4feb43bc4eb6c03e832f5fcd637d1abb44b98f9cfa245c58e27382f58859f8f6" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" mocktail: dependency: transitive description: @@ -929,10 +913,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.2" source_map_stack_trace: dependency: transitive description: @@ -985,10 +969,10 @@ packages: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: "9faa2fedc5385ef238ce772589f7718c24cdddd27419b609bb9c6f703ea27988" + sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.5" sqflite_darwin: dependency: transitive description: @@ -1009,10 +993,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" + sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb url: "https://pub.dev" source: hosted - version: "2.9.4" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -1065,34 +1049,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" + version: "0.6.12" typed_data: dependency: transitive description: @@ -1129,18 +1105,18 @@ packages: dependency: transitive description: name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" webkit_inspection_protocol: dependency: transitive description: