From 8c6bf598f5bd1149e23275ccddb2365fd8b65170 Mon Sep 17 00:00:00 2001 From: gitea Date: Sat, 8 Nov 2025 12:54:40 +0100 Subject: [PATCH] mudular code ready to fork --- lib/config/config_loader.dart | 16 +- lib/data/firebase/firebase_service.dart | 57 +++--- lib/data/immich/immich_service.dart | 102 +++++------ lib/data/nostr/nostr_service.dart | 19 +- lib/data/session/session_service.dart | 29 +-- lib/data/sync/sync_engine.dart | 13 +- lib/main.dart | 170 +++--------------- lib/ui/home/home_screen.dart | 33 ++-- lib/ui/immich/immich_screen.dart | 52 +++--- lib/ui/navigation/app_router.dart | 36 ++-- .../navigation/main_navigation_scaffold.dart | 61 ++----- lib/ui/nostr_events/nostr_events_screen.dart | 26 +-- lib/ui/session/session_screen.dart | 54 +++--- lib/ui/settings/settings_screen.dart | 90 +++++----- test/config/config_loader_test.dart | 1 + test/data/firebase/firebase_service_test.dart | 21 +-- test/data/immich/immich_service_test.dart | 1 + test/data/nostr/nostr_service_test.dart | 1 + test/data/session/session_service_test.dart | 1 + test/data/sync/sync_engine_test.dart | 1 + .../main_navigation_scaffold_test.dart | 23 ++- 21 files changed, 288 insertions(+), 519 deletions(-) diff --git a/lib/config/config_loader.dart b/lib/config/config_loader.dart index fb16e16..66b6b42 100644 --- a/lib/config/config_loader.dart +++ b/lib/config/config_loader.dart @@ -1,22 +1,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../core/exceptions/invalid_environment_exception.dart'; import 'app_config.dart'; import '../data/firebase/models/firebase_config.dart'; -/// Exception thrown when an invalid environment is provided to [ConfigLoader]. -class InvalidEnvironmentException implements Exception { - /// The invalid environment that was provided. - final String environment; - - /// Creates an [InvalidEnvironmentException] with the provided environment. - InvalidEnvironmentException(this.environment); - - @override - String toString() { - return 'InvalidEnvironmentException: Invalid environment "$environment". ' - 'Valid environments are: dev, prod'; - } -} - /// Loads application configuration based on the specified environment. /// /// This class provides a factory method to load configuration for either diff --git a/lib/data/firebase/firebase_service.dart b/lib/data/firebase/firebase_service.dart index 682b1b7..5cd41d6 100644 --- a/lib/data/firebase/firebase_service.dart +++ b/lib/data/firebase/firebase_service.dart @@ -5,22 +5,11 @@ import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; +import '../../core/exceptions/firebase_exception.dart' show FirebaseServiceException; import '../local/local_storage_service.dart'; import '../local/models/item.dart'; import 'models/firebase_config.dart'; -/// Exception thrown when Firebase operations fail. -class FirebaseException implements Exception { - /// Error message. - final String message; - - /// Creates a [FirebaseException] with the provided message. - FirebaseException(this.message); - - @override - String toString() => 'FirebaseException: $message'; -} - /// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics). /// /// This service provides: @@ -86,7 +75,7 @@ class FirebaseService { /// Must be called before using any Firebase services. /// If Firebase is disabled, this method does nothing. /// - /// Throws [FirebaseException] if initialization fails. + /// Throws [FirebaseServiceException] if initialization fails. Future initialize() async { if (!config.enabled) { return; // Firebase disabled, nothing to initialize @@ -135,7 +124,7 @@ class FirebaseService { _initialized = true; } catch (e) { - throw FirebaseException('Failed to initialize Firebase: $e'); + throw FirebaseServiceException('Failed to initialize Firebase: $e'); } } @@ -146,17 +135,17 @@ class FirebaseService { /// /// Returns the Firebase Auth user. /// - /// Throws [FirebaseException] if auth is disabled or login fails. + /// Throws [FirebaseServiceException] if auth is disabled or login fails. Future loginWithEmailPassword({ required String email, required String password, }) async { if (!config.enabled || !config.authEnabled) { - throw FirebaseException('Firebase Auth is not enabled'); + throw FirebaseServiceException('Firebase Auth is not enabled'); } if (!_initialized || _auth == null) { - throw FirebaseException( + throw FirebaseServiceException( 'Firebase not initialized. Call initialize() first.'); } @@ -168,20 +157,20 @@ class FirebaseService { _firebaseUser = credential.user; return _firebaseUser!; } catch (e) { - throw FirebaseException('Failed to login: $e'); + throw FirebaseServiceException('Failed to login: $e'); } } /// Logs out the current user. /// - /// Throws [FirebaseException] if auth is disabled or logout fails. + /// Throws [FirebaseServiceException] if auth is disabled or logout fails. Future logout() async { if (!config.enabled || !config.authEnabled) { - throw FirebaseException('Firebase Auth is not enabled'); + throw FirebaseServiceException('Firebase Auth is not enabled'); } if (!_initialized || _auth == null) { - throw FirebaseException( + throw FirebaseServiceException( 'Firebase not initialized. Call initialize() first.'); } @@ -189,7 +178,7 @@ class FirebaseService { await _auth!.signOut(); _firebaseUser = null; } catch (e) { - throw FirebaseException('Failed to logout: $e'); + throw FirebaseServiceException('Failed to logout: $e'); } } @@ -197,14 +186,14 @@ class FirebaseService { /// /// [userId] - User ID to associate items with (for multi-user support). /// - /// Throws [FirebaseException] if Firestore is disabled or sync fails. + /// Throws [FirebaseServiceException] if Firestore is disabled or sync fails. Future syncItemsToFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { - throw FirebaseException('Firestore is not enabled'); + throw FirebaseServiceException('Firestore is not enabled'); } if (!_initialized || _firestore == null) { - throw FirebaseException( + throw FirebaseServiceException( 'Firestore not initialized. Call initialize() first.'); } @@ -229,7 +218,7 @@ class FirebaseService { await batch.commit(); } catch (e) { - throw FirebaseException('Failed to sync items to Firestore: $e'); + throw FirebaseServiceException('Failed to sync items to Firestore: $e'); } } @@ -237,14 +226,14 @@ class FirebaseService { /// /// [userId] - User ID to fetch items for. /// - /// Throws [FirebaseException] if Firestore is disabled or sync fails. + /// Throws [FirebaseServiceException] if Firestore is disabled or sync fails. Future syncItemsFromFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { - throw FirebaseException('Firestore is not enabled'); + throw FirebaseServiceException('Firestore is not enabled'); } if (!_initialized || _firestore == null) { - throw FirebaseException( + throw FirebaseServiceException( 'Firestore not initialized. Call initialize() first.'); } @@ -271,7 +260,7 @@ class FirebaseService { } } } catch (e) { - throw FirebaseException('Failed to sync items from Firestore: $e'); + throw FirebaseServiceException('Failed to sync items from Firestore: $e'); } } @@ -282,14 +271,14 @@ class FirebaseService { /// /// Returns the download URL. /// - /// Throws [FirebaseException] if Storage is disabled or upload fails. + /// Throws [FirebaseServiceException] if Storage is disabled or upload fails. Future uploadFile(File file, String path) async { if (!config.enabled || !config.storageEnabled) { - throw FirebaseException('Firebase Storage is not enabled'); + throw FirebaseServiceException('Firebase Storage is not enabled'); } if (!_initialized || _storage == null) { - throw FirebaseException( + throw FirebaseServiceException( 'Firebase Storage not initialized. Call initialize() first.'); } @@ -298,7 +287,7 @@ class FirebaseService { await ref.putFile(file); return await ref.getDownloadURL(); } catch (e) { - throw FirebaseException('Failed to upload file: $e'); + throw FirebaseServiceException('Failed to upload file: $e'); } } diff --git a/lib/data/immich/immich_service.dart b/lib/data/immich/immich_service.dart index e36c482..bf93089 100644 --- a/lib/data/immich/immich_service.dart +++ b/lib/data/immich/immich_service.dart @@ -1,32 +1,14 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; +import '../../core/logger.dart'; +import '../../core/exceptions/immich_exception.dart'; import '../local/local_storage_service.dart'; import '../local/models/item.dart'; import 'models/immich_asset.dart'; import 'models/upload_response.dart'; -/// Exception thrown when Immich API operations fail. -class ImmichException implements Exception { - /// Error message. - final String message; - - /// HTTP status code if available. - final int? statusCode; - - /// Creates an [ImmichException] with the provided message. - ImmichException(this.message, [this.statusCode]); - - @override - String toString() { - if (statusCode != null) { - return 'ImmichException: $message (Status: $statusCode)'; - } - return 'ImmichException: $message'; - } -} - /// Service for interacting with Immich API. /// /// This service provides: @@ -175,15 +157,15 @@ class ImmichService { } final uploadUrl = '$_baseUrl$endpointPath'; - debugPrint('=== Immich Upload Request ==='); - debugPrint('URL: $uploadUrl'); - debugPrint('Base URL: $_baseUrl'); - debugPrint('File: $fileName, Size: ${await imageFile.length()} bytes'); - debugPrint('Device ID: $deviceId'); - debugPrint('Device Asset ID: $deviceAssetId'); - debugPrint('File Created At: $fileCreatedAtIso'); - debugPrint('File Modified At: $fileModifiedAtIso'); - debugPrint('Metadata: $metadataJson'); + Logger.debug('=== Immich Upload Request ==='); + Logger.debug('URL: $uploadUrl'); + Logger.debug('Base URL: $_baseUrl'); + Logger.debug('File: $fileName, Size: ${await imageFile.length()} bytes'); + Logger.debug('Device ID: $deviceId'); + Logger.debug('Device Asset ID: $deviceAssetId'); + Logger.debug('File Created At: $fileCreatedAtIso'); + Logger.debug('File Modified At: $fileModifiedAtIso'); + Logger.debug('Metadata: $metadataJson'); final response = await _dio.post( endpointPath, @@ -196,17 +178,17 @@ class ImmichService { ), ); - debugPrint('=== Immich Upload Response ==='); - debugPrint('Status Code: ${response.statusCode}'); - debugPrint('Response Data: ${response.data}'); - debugPrint('Response Headers: ${response.headers}'); + Logger.debug('=== Immich Upload Response ==='); + Logger.debug('Status Code: ${response.statusCode}'); + Logger.debug('Response Data: ${response.data}'); + Logger.debug('Response Headers: ${response.headers}'); if (response.statusCode != 200 && response.statusCode != 201) { final errorMessage = response.data is Map ? (response.data as Map)['message']?.toString() ?? response.statusMessage : response.statusMessage; - debugPrint( + Logger.error( 'Upload failed with status ${response.statusCode}: $errorMessage'); throw ImmichException( 'Upload failed: $errorMessage', @@ -216,15 +198,15 @@ class ImmichService { // Log the response data structure if (response.data is Map) { - debugPrint('Response is Map with keys: ${(response.data as Map).keys}'); - debugPrint('Full response map: ${response.data}'); + Logger.debug('Response is Map with keys: ${(response.data as Map).keys}'); + Logger.debug('Full response map: ${response.data}'); } else if (response.data is List) { - debugPrint( + Logger.debug( 'Response is List with ${(response.data as List).length} items'); - debugPrint('First item: ${(response.data as List).first}'); + Logger.debug('First item: ${(response.data as List).first}'); } else { - debugPrint('Response type: ${response.data.runtimeType}'); - debugPrint('Response value: ${response.data}'); + Logger.debug('Response type: ${response.data.runtimeType}'); + Logger.debug('Response value: ${response.data}'); } // Handle response - it might be a single object or an array @@ -232,7 +214,7 @@ class ImmichService { if (response.data is List && (response.data as List).isNotEmpty) { // If response is an array, take the first item responseData = (response.data as List).first as Map; - debugPrint('Using first item from array response'); + Logger.debug('Using first item from array response'); } else if (response.data is Map) { responseData = response.data as Map; } else { @@ -243,24 +225,24 @@ class ImmichService { } final uploadResponse = UploadResponse.fromJson(responseData); - debugPrint('Parsed Upload Response:'); - debugPrint(' ID: ${uploadResponse.id}'); - debugPrint(' Duplicate: ${uploadResponse.duplicate}'); + Logger.debug('Parsed Upload Response:'); + Logger.debug(' ID: ${uploadResponse.id}'); + Logger.debug(' Duplicate: ${uploadResponse.duplicate}'); // Fetch full asset details to store complete metadata - debugPrint('Fetching full asset details for ID: ${uploadResponse.id}'); + Logger.debug('Fetching full asset details for ID: ${uploadResponse.id}'); try { final asset = await _getAssetById(uploadResponse.id); - debugPrint('Fetched asset: ${asset.id}, ${asset.fileName}'); + Logger.debug('Fetched asset: ${asset.id}, ${asset.fileName}'); // Store metadata in local storage - debugPrint('Storing asset metadata in local storage'); + Logger.debug('Storing asset metadata in local storage'); await _storeAssetMetadata(asset); - debugPrint('Asset metadata stored successfully'); + Logger.debug('Asset metadata stored successfully'); } catch (e) { // Log error but don't fail the upload - asset was uploaded successfully - debugPrint('Warning: Failed to fetch/store asset metadata: $e'); - debugPrint('Upload was successful, but metadata caching failed'); + Logger.warning('Failed to fetch/store asset metadata: $e'); + Logger.warning('Upload was successful, but metadata caching failed'); } return uploadResponse; @@ -558,9 +540,9 @@ class ImmichService { } try { - debugPrint('=== Immich Delete Assets ==='); - debugPrint('Asset IDs to delete: $assetIds'); - debugPrint('Count: ${assetIds.length}'); + Logger.debug('=== Immich Delete Assets ==='); + Logger.debug('Asset IDs to delete: $assetIds'); + Logger.debug('Count: ${assetIds.length}'); // DELETE /api/assets with ids in request body // According to Immich API: DELETE /api/assets with body: {"ids": ["uuid1", "uuid2", ...]} @@ -568,7 +550,7 @@ class ImmichService { 'ids': assetIds, }; - debugPrint('Request body: $requestBody'); + Logger.debug('Request body: $requestBody'); final response = await _dio.delete( '/api/assets', @@ -581,9 +563,9 @@ class ImmichService { ), ); - debugPrint('=== Immich Delete Response ==='); - debugPrint('Status Code: ${response.statusCode}'); - debugPrint('Response Data: ${response.data}'); + Logger.debug('=== Immich Delete Response ==='); + Logger.debug('Status Code: ${response.statusCode}'); + Logger.debug('Response Data: ${response.data}'); if (response.statusCode != 200 && response.statusCode != 204) { final errorMessage = response.data is Map @@ -601,11 +583,11 @@ class ImmichService { try { await _localStorage.deleteItem('immich_$assetId'); } catch (e) { - debugPrint('Warning: Failed to remove asset $assetId from cache: $e'); + Logger.warning('Failed to remove asset $assetId from cache: $e'); } } - debugPrint('Successfully deleted ${assetIds.length} asset(s)'); + Logger.info('Successfully deleted ${assetIds.length} asset(s)'); } on DioException catch (e) { final statusCode = e.response?.statusCode; final errorData = e.response?.data; diff --git a/lib/data/nostr/nostr_service.dart b/lib/data/nostr/nostr_service.dart index 33bd265..f56205d 100644 --- a/lib/data/nostr/nostr_service.dart +++ b/lib/data/nostr/nostr_service.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:nostr_tools/nostr_tools.dart'; import 'package:http/http.dart' as http; +import '../../core/logger.dart'; +import '../../core/exceptions/nostr_exception.dart'; import 'models/nostr_keypair.dart'; import 'models/nostr_event.dart'; import 'models/nostr_relay.dart'; import 'models/nostr_profile.dart'; -/// Exception thrown when Nostr operations fail. -class NostrException implements Exception { - /// Error message. - final String message; - - /// Creates a [NostrException] with the provided message. - NostrException(this.message); - - @override - String toString() => 'NostrException: $message'; -} - /// Service for interacting with Nostr protocol. /// /// This service provides: @@ -492,7 +481,7 @@ class NostrService { } } catch (e) { // Ignore parsing errors - debugPrint('Error parsing profile event: $e'); + Logger.warning('Error parsing profile event: $e'); } } else if (message['type'] == 'EOSE' && message['subscription_id'] == reqId) { @@ -634,7 +623,7 @@ class NostrService { addedCount++; } catch (e) { // Skip invalid relay URLs - debugPrint('Warning: Invalid relay URL from NIP-05: $relayUrl'); + Logger.warning('Invalid relay URL from NIP-05: $relayUrl'); } } diff --git a/lib/data/session/session_service.dart b/lib/data/session/session_service.dart index b0a0224..d876018 100644 --- a/lib/data/session/session_service.dart +++ b/lib/data/session/session_service.dart @@ -1,7 +1,8 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import '../../core/logger.dart'; +import '../../core/exceptions/session_exception.dart'; import '../local/local_storage_service.dart'; import '../sync/sync_engine.dart'; import '../firebase/firebase_service.dart'; @@ -10,18 +11,6 @@ import '../nostr/models/nostr_keypair.dart'; import '../nostr/models/nostr_profile.dart'; import 'models/user.dart'; -/// Exception thrown when session operations fail. -class SessionException implements Exception { - /// Error message. - final String message; - - /// Creates a [SessionException] with the provided message. - SessionException(this.message); - - @override - String toString() => 'SessionException: $message'; -} - /// Service for managing user sessions, login, logout, and session isolation. /// /// This service provides: @@ -124,7 +113,7 @@ class SessionService { await _firebaseService!.syncItemsFromFirestore(user.id); } catch (e) { // Log error but don't fail login - offline-first behavior - debugPrint('Warning: Failed to sync from Firebase on login: $e'); + Logger.warning('Failed to sync from Firebase on login: $e'); } } @@ -174,7 +163,7 @@ class SessionService { try { profile = await _nostrService!.fetchProfile(keyPair.publicKey); } catch (e) { - debugPrint('Warning: Failed to fetch Nostr profile: $e'); + Logger.warning('Failed to fetch Nostr profile: $e'); // Continue without profile - offline-first behavior } @@ -195,7 +184,7 @@ class SessionService { await _firebaseService!.syncItemsFromFirestore(user.id); } catch (e) { // Log error but don't fail login - offline-first behavior - debugPrint('Warning: Failed to sync from Firebase on login: $e'); + Logger.warning('Failed to sync from Firebase on login: $e'); } } @@ -230,7 +219,7 @@ class SessionService { await _firebaseService!.syncItemsToFirestore(userId); } catch (e) { // Log error but don't fail logout - offline-first behavior - debugPrint('Warning: Failed to sync to Firebase on logout: $e'); + Logger.warning('Failed to sync to Firebase on logout: $e'); } } @@ -407,7 +396,7 @@ class SessionService { try { profile = await _nostrService!.fetchProfile(_currentUser!.id); } catch (e) { - debugPrint('Warning: Failed to refresh Nostr profile: $e'); + Logger.warning('Failed to refresh Nostr profile: $e'); // Continue without profile update - offline-first behavior } @@ -447,11 +436,11 @@ class SessionService { ); if (addedCount > 0) { - debugPrint('Loaded $addedCount preferred relay(s) from NIP-05: $nip05'); + Logger.info('Loaded $addedCount preferred relay(s) from NIP-05: $nip05'); } } catch (e) { // Log error but don't fail - offline-first behavior - debugPrint('Warning: Failed to load preferred relays from NIP-05: $e'); + Logger.warning('Failed to load preferred relays from NIP-05: $e'); } } } diff --git a/lib/data/sync/sync_engine.dart b/lib/data/sync/sync_engine.dart index 9577d45..5341e18 100644 --- a/lib/data/sync/sync_engine.dart +++ b/lib/data/sync/sync_engine.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../../core/exceptions/sync_exception.dart'; import '../local/local_storage_service.dart'; import '../immich/immich_service.dart'; import '../nostr/nostr_service.dart'; @@ -6,18 +7,6 @@ import '../nostr/models/nostr_keypair.dart'; import 'models/sync_status.dart'; import 'models/sync_operation.dart'; -/// Exception thrown when sync operations fail. -class SyncException implements Exception { - /// Error message. - final String message; - - /// Creates a [SyncException] with the provided message. - SyncException(this.message); - - @override - String toString() => 'SyncException: $message'; -} - /// Engine for coordinating data synchronization between local storage, Immich, and Nostr. /// /// This service provides: diff --git a/lib/main.dart b/lib/main.dart index 0f93674..64fa5b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,182 +1,70 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'config/config_loader.dart'; -import 'data/local/local_storage_service.dart'; -import 'data/nostr/nostr_service.dart'; -import 'data/sync/sync_engine.dart'; -import 'data/firebase/firebase_service.dart'; -import 'data/session/session_service.dart'; -import 'data/immich/immich_service.dart'; +import 'core/app_initializer.dart'; +import 'core/app_services.dart'; +import 'core/logger.dart'; import 'ui/navigation/main_navigation_scaffold.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - - // Load .env file (optional - falls back to defaults if not found) - try { - await dotenv.load(fileName: '.env'); - } catch (e) { - debugPrint('Note: .env file not found, using default values: $e'); - } - // Load configuration based on environment + // Determine environment const String environment = String.fromEnvironment( 'ENV', defaultValue: 'dev', ); - final config = ConfigLoader.load(environment); - - // Initialize Firebase if enabled - if (config.firebaseConfig.enabled) { - try { - await Firebase.initializeApp(); - if (config.enableLogging) { - debugPrint('Firebase initialized successfully'); - } - } catch (e) { - debugPrint('Firebase initialization failed: $e'); - debugPrint('Note: Firebase requires google-services.json (Android) and GoogleService-Info.plist (iOS)'); - } - } - - if (config.enableLogging) { - debugPrint('App initialized with config: $config'); + // Initialize all services + AppServices? appServices; + try { + appServices = await AppInitializer.initialize(environment: environment); + } catch (e, stackTrace) { + Logger.error('Failed to initialize application', e, stackTrace); + // App will show error state } - runApp(const MyApp()); + runApp(MyApp(appServices: appServices)); } /// The root widget of the application. class MyApp extends StatefulWidget { - const MyApp({super.key}); + final AppServices? appServices; + + const MyApp({super.key, this.appServices}); @override State createState() => _MyAppState(); } class _MyAppState extends State { - LocalStorageService? _storageService; - NostrService? _nostrService; - SyncEngine? _syncEngine; - FirebaseService? _firebaseService; - SessionService? _sessionService; - ImmichService? _immichService; - bool _isInitialized = false; - - @override - void initState() { - super.initState(); - _initializeStorage(); - } - - Future _initializeStorage() async { - try { - _storageService = LocalStorageService(); - await _storageService!.initialize(); - - // Initialize Nostr service and sync engine - _nostrService = NostrService(); - final nostrKeyPair = _nostrService!.generateKeyPair(); - _syncEngine = SyncEngine( - localStorage: _storageService!, - nostrService: _nostrService!, - nostrKeyPair: nostrKeyPair, - ); - - // Load relays from config - final config = ConfigLoader.load( - const String.fromEnvironment('ENV', defaultValue: 'dev'), - ); - for (final relayUrl in config.nostrRelays) { - _nostrService!.addRelay(relayUrl); - } - - // Initialize Immich service - _immichService = ImmichService( - baseUrl: config.immichBaseUrl, - apiKey: config.immichApiKey, - localStorage: _storageService!, - ); - - // Initialize Firebase service if enabled - if (config.firebaseConfig.enabled) { - try { - _firebaseService = FirebaseService( - config: config.firebaseConfig, - localStorage: _storageService!, - ); - await _firebaseService!.initialize(); - if (config.enableLogging) { - debugPrint('Firebase service initialized: ${_firebaseService!.isEnabled}'); - } - } catch (e) { - debugPrint('Firebase service initialization failed: $e'); - _firebaseService = null; - } - } - - // Initialize SessionService with Firebase and Nostr integration - _sessionService = SessionService( - localStorage: _storageService!, - syncEngine: _syncEngine, - firebaseService: _firebaseService, - nostrService: _nostrService, - ); - - setState(() { - _isInitialized = true; - }); - } catch (e) { - debugPrint('Failed to initialize storage: $e'); - // Reset to null if initialization failed - _storageService = null; - _nostrService = null; - _syncEngine = null; - _firebaseService = null; - _sessionService = null; - _immichService = null; - } - } - - @override void dispose() { - _syncEngine?.dispose(); - _nostrService?.dispose(); - _firebaseService?.dispose(); - // Only close if storage service was initialized - if (_storageService != null) { - try { - _storageService!.close(); - } catch (e) { - debugPrint('Error closing storage service: $e'); - } - } + // Dispose of all services + widget.appServices?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final appServices = widget.appServices; + return MaterialApp( title: 'App Boilerplate', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), - home: _isInitialized - ? MainNavigationScaffold( - sessionService: _sessionService, - localStorageService: _storageService, - nostrService: _nostrService, - syncEngine: _syncEngine, - firebaseService: _firebaseService, - immichService: _immichService, - ) + home: appServices != null + ? const MainNavigationScaffold() : const Scaffold( body: Center( - child: CircularProgressIndicator(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Initializing application...'), + ], + ), ), ), ); diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 35d9bc1..798c6cb 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; +import '../../core/service_locator.dart'; import '../../data/local/local_storage_service.dart'; import '../../data/local/models/item.dart'; /// Home screen showing local storage and cached content. class HomeScreen extends StatefulWidget { - final LocalStorageService? localStorageService; - - const HomeScreen({ - super.key, - this.localStorageService, - }); + const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); @@ -26,15 +22,16 @@ class _HomeScreenState extends State { } Future _loadItems() async { - if (widget.localStorageService == null) { - setState(() { - _isLoading = false; - }); - return; - } - try { - final items = await widget.localStorageService!.getAllItems(); + final localStorageService = ServiceLocator.instance.localStorageService; + if (localStorageService == null) { + setState(() { + _isLoading = false; + }); + return; + } + + final items = await localStorageService.getAllItems(); setState(() { _items = items; _isLoading = false; @@ -90,7 +87,8 @@ class _HomeScreenState extends State { trailing: IconButton( icon: const Icon(Icons.delete_outline), onPressed: () async { - await widget.localStorageService?.deleteItem(item.id); + final localStorageService = ServiceLocator.instance.localStorageService; + await localStorageService?.deleteItem(item.id); _loadItems(); }, ), @@ -98,9 +96,10 @@ class _HomeScreenState extends State { }, ), ), - floatingActionButton: widget.localStorageService != null + floatingActionButton: ServiceLocator.instance.localStorageService != null ? FloatingActionButton( onPressed: () async { + final localStorageService = ServiceLocator.instance.localStorageService; final item = Item( id: 'item-${DateTime.now().millisecondsSinceEpoch}', data: { @@ -108,7 +107,7 @@ class _HomeScreenState extends State { 'timestamp': DateTime.now().toIso8601String(), }, ); - await widget.localStorageService!.insertItem(item); + await localStorageService!.insertItem(item); _loadItems(); }, child: const Icon(Icons.add), diff --git a/lib/ui/immich/immich_screen.dart b/lib/ui/immich/immich_screen.dart index 5468b23..4c0f0d8 100644 --- a/lib/ui/immich/immich_screen.dart +++ b/lib/ui/immich/immich_screen.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import '../../core/logger.dart'; +import '../../core/service_locator.dart'; import '../../data/local/local_storage_service.dart'; import '../../data/immich/immich_service.dart'; import '../../data/immich/models/immich_asset.dart'; @@ -11,14 +13,7 @@ import '../../data/immich/models/immich_asset.dart'; /// Displays images from Immich in a grid layout with pull-to-refresh. /// Shows cached images first, then fetches from API. class ImmichScreen extends StatefulWidget { - final LocalStorageService? localStorageService; - final ImmichService? immichService; - - const ImmichScreen({ - super.key, - this.localStorageService, - this.immichService, - }); + const ImmichScreen({super.key}); @override State createState() => _ImmichScreenState(); @@ -41,7 +36,7 @@ class _ImmichScreenState extends State { /// Loads assets from cache first, then fetches from API. Future _loadAssets({bool forceRefresh = false}) async { - if (widget.immichService == null) { + if (ServiceLocator.instance.immichService == null) { setState(() { _errorMessage = 'Immich service not available'; _isLoading = false; @@ -57,7 +52,7 @@ class _ImmichScreenState extends State { try { // First, try to load cached assets if (!forceRefresh) { - final cachedAssets = await widget.immichService!.getCachedAssets(); + final cachedAssets = await ServiceLocator.instance.immichService!.getCachedAssets(); if (cachedAssets.isNotEmpty) { setState(() { _assets = cachedAssets; @@ -82,7 +77,7 @@ class _ImmichScreenState extends State { /// Fetches assets from Immich API. Future _fetchFromApi() async { try { - final assets = await widget.immichService!.fetchAssets(limit: 100); + final assets = await ServiceLocator.instance.immichService!.fetchAssets(limit: 100); setState(() { _assets = assets; _isLoading = false; @@ -97,7 +92,7 @@ class _ImmichScreenState extends State { /// Gets the thumbnail URL for an asset with proper headers. String _getThumbnailUrl(ImmichAsset asset) { - return widget.immichService!.getThumbnailUrl(asset.id); + return ServiceLocator.instance.immichService!.getThumbnailUrl(asset.id); } @override @@ -305,7 +300,7 @@ class _ImmichScreenState extends State { // Use FutureBuilder to fetch image bytes via ImmichService with proper auth return FutureBuilder( future: - widget.immichService?.fetchImageBytes(asset.id, isThumbnail: true), + ServiceLocator.instance.immichService?.fetchImageBytes(asset.id, isThumbnail: true), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( @@ -350,7 +345,7 @@ class _ImmichScreenState extends State { ), Expanded( child: FutureBuilder( - future: widget.immichService + future: ServiceLocator.instance.immichService ?.fetchImageBytes(asset.id, isThumbnail: false), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -405,7 +400,7 @@ class _ImmichScreenState extends State { /// Tests the connection to Immich server by calling /api/server/about. Future _testServerConnection() async { - if (widget.immichService == null) { + if (ServiceLocator.instance.immichService == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -426,7 +421,7 @@ class _ImmichScreenState extends State { ); try { - final serverInfo = await widget.immichService!.getServerInfo(); + final serverInfo = await ServiceLocator.instance.immichService!.getServerInfo(); if (!mounted) return; @@ -493,7 +488,7 @@ class _ImmichScreenState extends State { } Future _deleteSelectedAssets() async { - if (_selectedAssetIds.isEmpty || widget.immichService == null) { + if (_selectedAssetIds.isEmpty || ServiceLocator.instance.immichService == null) { return; } @@ -527,7 +522,7 @@ class _ImmichScreenState extends State { try { final assetIdsToDelete = _selectedAssetIds.toList(); - await widget.immichService!.deleteAssets(assetIdsToDelete); + await ServiceLocator.instance.immichService!.deleteAssets(assetIdsToDelete); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -585,7 +580,7 @@ class _ImmichScreenState extends State { /// Opens image picker and uploads selected images to Immich. Future _pickAndUploadImages() async { - if (widget.immichService == null) { + if (ServiceLocator.instance.immichService == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -644,8 +639,7 @@ class _ImmichScreenState extends State { } } catch (pickError, stackTrace) { // Handle image picker specific errors - debugPrint('Image picker error: $pickError'); - debugPrint('Stack trace: $stackTrace'); + Logger.error('Image picker error: $pickError', pickError, stackTrace); if (mounted) { String errorMessage = 'Failed to open gallery'; @@ -732,13 +726,12 @@ class _ImmichScreenState extends State { for (final pickedFile in pickedFiles) { try { final file = File(pickedFile.path); - debugPrint('Uploading file: ${pickedFile.name}'); - final uploadResponse = await widget.immichService!.uploadImage(file); - debugPrint('Upload successful for ${pickedFile.name}: ${uploadResponse.id}'); + Logger.debug('Uploading file: ${pickedFile.name}'); + final uploadResponse = await ServiceLocator.instance.immichService!.uploadImage(file); + Logger.info('Upload successful for ${pickedFile.name}: ${uploadResponse.id}'); successCount++; } catch (e, stackTrace) { - debugPrint('Upload failed for ${pickedFile.name}: $e'); - debugPrint('Stack trace: $stackTrace'); + Logger.error('Upload failed for ${pickedFile.name}: $e', e, stackTrace); failureCount++; errors.add('${pickedFile.name}: ${e.toString()}'); } @@ -788,9 +781,9 @@ class _ImmichScreenState extends State { } // Refresh the asset list - debugPrint('Refreshing asset list after upload'); + Logger.debug('Refreshing asset list after upload'); await _loadAssets(forceRefresh: true); - debugPrint('Asset list refreshed, current count: ${_assets.length}'); + Logger.debug('Asset list refreshed, current count: ${_assets.length}'); } } catch (e, stackTrace) { setState(() { @@ -799,8 +792,7 @@ class _ImmichScreenState extends State { if (mounted) { // Log the full error for debugging - debugPrint('Image picker error: $e'); - debugPrint('Stack trace: $stackTrace'); + Logger.error('Image picker error: $e', e, stackTrace); String errorMessage = 'Failed to pick images'; if (e.toString().contains('Permission')) { diff --git a/lib/ui/navigation/app_router.dart b/lib/ui/navigation/app_router.dart index f8c1b14..74859d1 100644 --- a/lib/ui/navigation/app_router.dart +++ b/lib/ui/navigation/app_router.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../core/service_locator.dart'; import '../home/home_screen.dart'; import '../immich/immich_screen.dart'; import '../nostr_events/nostr_events_screen.dart'; @@ -6,11 +7,6 @@ import '../relay_management/relay_management_screen.dart'; import '../relay_management/relay_management_controller.dart'; import '../session/session_screen.dart'; import '../settings/settings_screen.dart'; -import '../../data/nostr/nostr_service.dart'; -import '../../data/sync/sync_engine.dart'; -import '../../data/session/session_service.dart'; -import '../../data/local/local_storage_service.dart'; -import '../../data/firebase/firebase_service.dart'; /// Route names for the app navigation. class AppRoutes { @@ -86,31 +82,25 @@ class AppRouter { switch (settings.name) { case AppRoutes.home: return MaterialPageRoute( - builder: (_) => HomeScreen( - localStorageService: localStorageService, - ), + builder: (_) => const HomeScreen(), settings: settings, ); case AppRoutes.immich: return MaterialPageRoute( - builder: (_) => ImmichScreen( - localStorageService: localStorageService, - ), + builder: (_) => const ImmichScreen(), settings: settings, ); case AppRoutes.nostrEvents: return MaterialPageRoute( - builder: (_) => NostrEventsScreen( - nostrService: nostrService, - syncEngine: syncEngine, - sessionService: sessionService, - ), + builder: (_) => const NostrEventsScreen(), settings: settings, ); case AppRoutes.relayManagement: + final nostrService = ServiceLocator.instance.nostrService; + final syncEngine = ServiceLocator.instance.syncEngine; if (nostrService == null || syncEngine == null) { return MaterialPageRoute( builder: (_) => _buildErrorScreen('Nostr service not available'), @@ -120,8 +110,8 @@ class AppRouter { return MaterialPageRoute( builder: (_) => RelayManagementScreen( controller: RelayManagementController( - nostrService: nostrService!, - syncEngine: syncEngine!, + nostrService: nostrService, + syncEngine: syncEngine, ), ), settings: settings, @@ -129,19 +119,13 @@ class AppRouter { case AppRoutes.session: return MaterialPageRoute( - builder: (_) => SessionScreen( - sessionService: sessionService, - firebaseService: firebaseService, - nostrService: nostrService, - ), + builder: (_) => const SessionScreen(), settings: settings, ); case AppRoutes.settings: return MaterialPageRoute( - builder: (_) => SettingsScreen( - firebaseService: firebaseService, - ), + builder: (_) => const SettingsScreen(), settings: settings, ); diff --git a/lib/ui/navigation/main_navigation_scaffold.dart b/lib/ui/navigation/main_navigation_scaffold.dart index fb16553..10bb49b 100644 --- a/lib/ui/navigation/main_navigation_scaffold.dart +++ b/lib/ui/navigation/main_navigation_scaffold.dart @@ -4,31 +4,11 @@ import '../immich/immich_screen.dart'; import '../nostr_events/nostr_events_screen.dart'; import '../session/session_screen.dart'; import '../settings/settings_screen.dart'; -import '../../data/session/session_service.dart'; -import '../../data/local/local_storage_service.dart'; -import '../../data/nostr/nostr_service.dart'; -import '../../data/sync/sync_engine.dart'; -import '../../data/firebase/firebase_service.dart'; -import '../../data/immich/immich_service.dart'; +import '../../core/service_locator.dart'; /// Main navigation scaffold with bottom navigation bar. class MainNavigationScaffold extends StatefulWidget { - final SessionService? sessionService; - final LocalStorageService? localStorageService; - final NostrService? nostrService; - final SyncEngine? syncEngine; - final FirebaseService? firebaseService; - final ImmichService? immichService; - - const MainNavigationScaffold({ - super.key, - this.sessionService, - this.localStorageService, - this.nostrService, - this.syncEngine, - this.firebaseService, - this.immichService, - }); + const MainNavigationScaffold({super.key}); @override State createState() => _MainNavigationScaffoldState(); @@ -43,7 +23,8 @@ class _MainNavigationScaffoldState extends State { setState(() { _currentIndex = index; // If accessing a protected route (Immich=1, Nostr=2) while not logged in, remember it - if ((index == 1 || index == 2) && !(widget.sessionService?.isLoggedIn ?? false)) { + final sessionService = ServiceLocator.instance.sessionService; + if ((index == 1 || index == 2) && !(sessionService?.isLoggedIn ?? false)) { _pendingProtectedRoute = index; } else { _pendingProtectedRoute = null; @@ -57,7 +38,8 @@ class _MainNavigationScaffoldState extends State { _loginStateVersion++; // Force rebuild when login state changes // If user just logged in and was trying to access a protected route, navigate there - if (widget.sessionService?.isLoggedIn == true && _pendingProtectedRoute != null) { + final sessionService = ServiceLocator.instance.sessionService; + if (sessionService?.isLoggedIn == true && _pendingProtectedRoute != null) { _currentIndex = _pendingProtectedRoute!; _pendingProtectedRoute = null; } @@ -65,43 +47,30 @@ class _MainNavigationScaffoldState extends State { } Widget _buildScreen(int index) { + final locator = ServiceLocator.instance; + final sessionService = locator.sessionService; + switch (index) { case 0: - return HomeScreen( - localStorageService: widget.localStorageService, - ); + return const HomeScreen(); case 1: // Check auth guard for Immich - if (!(widget.sessionService?.isLoggedIn ?? false)) { + if (!(sessionService?.isLoggedIn ?? false)) { return _buildLoginRequiredScreen(); } - return ImmichScreen( - localStorageService: widget.localStorageService, - immichService: widget.immichService, - ); + return const ImmichScreen(); case 2: // Check auth guard for Nostr Events - if (!(widget.sessionService?.isLoggedIn ?? false)) { + if (!(sessionService?.isLoggedIn ?? false)) { return _buildLoginRequiredScreen(); } - return NostrEventsScreen( - nostrService: widget.nostrService, - syncEngine: widget.syncEngine, - sessionService: widget.sessionService, - ); + return const NostrEventsScreen(); case 3: return SessionScreen( - sessionService: widget.sessionService, - firebaseService: widget.firebaseService, - nostrService: widget.nostrService, onSessionChanged: _onSessionStateChanged, ); case 4: - return SettingsScreen( - firebaseService: widget.firebaseService, - nostrService: widget.nostrService, - syncEngine: widget.syncEngine, - ); + return const SettingsScreen(); default: return const SizedBox(); } diff --git a/lib/ui/nostr_events/nostr_events_screen.dart b/lib/ui/nostr_events/nostr_events_screen.dart index 81c509b..dadd294 100644 --- a/lib/ui/nostr_events/nostr_events_screen.dart +++ b/lib/ui/nostr_events/nostr_events_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../core/service_locator.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/sync/sync_engine.dart'; import '../../data/session/session_service.dart'; @@ -9,16 +10,7 @@ import '../relay_management/relay_management_controller.dart'; /// Screen for displaying and testing Nostr events. class NostrEventsScreen extends StatefulWidget { - final NostrService? nostrService; - final SyncEngine? syncEngine; - final SessionService? sessionService; - - const NostrEventsScreen({ - super.key, - this.nostrService, - this.syncEngine, - this.sessionService, - }); + const NostrEventsScreen({super.key}); @override State createState() => _NostrEventsScreenState(); @@ -29,7 +21,7 @@ class _NostrEventsScreenState extends State { bool _isLoading = false; Future _publishTestEvent() async { - if (widget.nostrService == null) { + if (ServiceLocator.instance.nostrService == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -42,7 +34,7 @@ class _NostrEventsScreenState extends State { } // Check if user is logged in with Nostr - final currentUser = widget.sessionService?.currentUser; + final currentUser = ServiceLocator.instance.sessionService?.currentUser; if (currentUser == null || currentUser.nostrPrivateKey == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -56,7 +48,7 @@ class _NostrEventsScreenState extends State { } // Get relays - final relays = widget.nostrService!.getRelays(); + final relays = ServiceLocator.instance.nostrService!.getRelays(); if (relays.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -85,7 +77,7 @@ class _NostrEventsScreenState extends State { ); // Publish to all enabled relays - final results = await widget.nostrService!.publishEventToAllRelays(event); + final results = await ServiceLocator.instance.nostrService!.publishEventToAllRelays(event); setState(() { _isLoading = false; @@ -169,13 +161,13 @@ class _NostrEventsScreenState extends State { const SizedBox(height: 8), TextButton.icon( onPressed: () { - if (widget.nostrService != null && widget.syncEngine != null) { + if (ServiceLocator.instance.nostrService != null && ServiceLocator.instance.syncEngine != null) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => RelayManagementScreen( controller: RelayManagementController( - nostrService: widget.nostrService!, - syncEngine: widget.syncEngine!, + nostrService: ServiceLocator.instance.nostrService!, + syncEngine: ServiceLocator.instance.syncEngine!, ), ), ), diff --git a/lib/ui/session/session_screen.dart b/lib/ui/session/session_screen.dart index 6e2246c..12d6678 100644 --- a/lib/ui/session/session_screen.dart +++ b/lib/ui/session/session_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/logger.dart'; +import '../../core/service_locator.dart'; import '../../data/session/session_service.dart'; import '../../data/firebase/firebase_service.dart'; import '../../data/nostr/nostr_service.dart'; @@ -6,16 +8,10 @@ import '../../data/nostr/models/nostr_keypair.dart'; /// Screen for user session management (login/logout). class SessionScreen extends StatefulWidget { - final SessionService? sessionService; - final FirebaseService? firebaseService; - final NostrService? nostrService; final VoidCallback? onSessionChanged; const SessionScreen({ super.key, - this.sessionService, - this.firebaseService, - this.nostrService, this.onSessionChanged, }); @@ -38,8 +34,8 @@ class _SessionScreenState extends State { void initState() { super.initState(); // Check if Firebase Auth is available - _useFirebaseAuth = widget.firebaseService?.isEnabled == true && - widget.firebaseService?.config.authEnabled == true; + _useFirebaseAuth = ServiceLocator.instance.firebaseService?.isEnabled == true && + ServiceLocator.instance.firebaseService?.config.authEnabled == true; } @override @@ -53,7 +49,7 @@ class _SessionScreenState extends State { } Future _handleLogin() async { - if (widget.sessionService == null) return; + if (ServiceLocator.instance.sessionService == null) return; setState(() { _isLoading = true; @@ -89,7 +85,7 @@ class _SessionScreenState extends State { } // Login with Nostr - await widget.sessionService!.loginWithNostr(nostrKey); + await ServiceLocator.instance.sessionService!.loginWithNostr(nostrKey); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -104,7 +100,7 @@ class _SessionScreenState extends State { } // Handle Firebase or regular login - if (_useFirebaseAuth && widget.firebaseService != null) { + if (_useFirebaseAuth && ServiceLocator.instance.firebaseService != null) { // Use Firebase Auth for authentication final email = _emailController.text.trim(); final password = _passwordController.text.trim(); @@ -121,13 +117,13 @@ class _SessionScreenState extends State { } // Authenticate with Firebase - final firebaseUser = await widget.firebaseService!.loginWithEmailPassword( + final firebaseUser = await ServiceLocator.instance.firebaseService!.loginWithEmailPassword( email: email, password: password, ); // Create session with Firebase user info - await widget.sessionService!.login( + await ServiceLocator.instance.sessionService!.login( id: firebaseUser.uid, username: firebaseUser.email?.split('@').first ?? firebaseUser.uid, token: await firebaseUser.getIdToken(), @@ -183,7 +179,7 @@ class _SessionScreenState extends State { } // Create session (demo mode - no real authentication) - await widget.sessionService!.login( + await ServiceLocator.instance.sessionService!.login( id: userId, username: username, ); @@ -204,7 +200,7 @@ class _SessionScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Login failed: ${e.toString().replaceAll('FirebaseException: ', '').replaceAll('SessionException: ', '')}'), + content: Text('Login failed: ${e.toString().replaceAll('FirebaseServiceException: ', '').replaceAll('SessionException: ', '')}'), backgroundColor: Colors.red, ), ); @@ -219,7 +215,7 @@ class _SessionScreenState extends State { } Future _handleLogout() async { - if (widget.sessionService == null) return; + if (ServiceLocator.instance.sessionService == null) return; setState(() { _isLoading = true; @@ -227,15 +223,15 @@ class _SessionScreenState extends State { try { // Logout from session service first - await widget.sessionService!.logout(); + await ServiceLocator.instance.sessionService!.logout(); // Also logout from Firebase Auth if enabled - if (_useFirebaseAuth && widget.firebaseService != null) { + if (_useFirebaseAuth && ServiceLocator.instance.firebaseService != null) { try { - await widget.firebaseService!.logout(); + await ServiceLocator.instance.firebaseService!.logout(); } catch (e) { // Log error but don't fail logout - session is already cleared - debugPrint('Warning: Firebase logout failed: $e'); + Logger.warning('Firebase logout failed: $e'); } } @@ -268,10 +264,10 @@ class _SessionScreenState extends State { } Future _handleRefresh() async { - if (widget.sessionService == null) return; + if (ServiceLocator.instance.sessionService == null) return; try { - await widget.sessionService!.refreshNostrProfile(); + await ServiceLocator.instance.sessionService!.refreshNostrProfile(); if (mounted) { setState(() {}); ScaffoldMessenger.of(context).showSnackBar( @@ -296,8 +292,8 @@ class _SessionScreenState extends State { @override Widget build(BuildContext context) { - final isLoggedIn = widget.sessionService?.isLoggedIn ?? false; - final currentUser = widget.sessionService?.currentUser; + final isLoggedIn = ServiceLocator.instance.sessionService?.isLoggedIn ?? false; + final currentUser = ServiceLocator.instance.sessionService?.currentUser; return Scaffold( appBar: AppBar( @@ -389,7 +385,7 @@ class _SessionScreenState extends State { _Nip05Section( nip05: currentUser.nostrProfile!.nip05!, publicKey: currentUser.id, - nostrService: widget.nostrService, + nostrService: ServiceLocator.instance.nostrService, ), if (currentUser.nostrProfile?.nip05 != null && currentUser.nostrProfile!.nip05!.isNotEmpty) @@ -476,7 +472,7 @@ class _SessionScreenState extends State { const SizedBox(height: 24), if (_useNostrLogin) ...[ // Key pair generation section - if (widget.nostrService != null) ...[ + if (ServiceLocator.instance.nostrService != null) ...[ Card( child: Padding( padding: const EdgeInsets.all(16), @@ -496,7 +492,7 @@ class _SessionScreenState extends State { ElevatedButton.icon( onPressed: () { setState(() { - _generatedKeyPair = widget.nostrService!.generateKeyPair(); + _generatedKeyPair = ServiceLocator.instance.nostrService!.generateKeyPair(); }); }, icon: const Icon(Icons.refresh, size: 18), @@ -691,7 +687,7 @@ class _Nip05SectionState extends State<_Nip05Section> { } Future _loadPreferredRelays() async { - if (widget.nostrService == null) { + if (ServiceLocator.instance.nostrService == null) { setState(() { _error = 'Nostr service not available'; }); @@ -704,7 +700,7 @@ class _Nip05SectionState extends State<_Nip05Section> { }); try { - final relays = await widget.nostrService!.fetchPreferredRelaysFromNip05( + final relays = await ServiceLocator.instance.nostrService!.fetchPreferredRelaysFromNip05( widget.nip05, widget.publicKey, ); diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index fcba6d1..5667fa4 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../core/service_locator.dart'; import '../../data/firebase/firebase_service.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/sync/sync_engine.dart'; @@ -7,16 +8,7 @@ import '../relay_management/relay_management_controller.dart'; /// Settings screen (placeholder). class SettingsScreen extends StatelessWidget { - final FirebaseService? firebaseService; - final NostrService? nostrService; - final SyncEngine? syncEngine; - - const SettingsScreen({ - super.key, - this.firebaseService, - this.nostrService, - this.syncEngine, - }); + const SettingsScreen({super.key}); @override Widget build(BuildContext context) { @@ -26,38 +18,56 @@ class SettingsScreen extends StatelessWidget { ), body: ListView( children: [ - if (firebaseService != null) - SwitchListTile( - title: const Text('Firebase Enabled'), - subtitle: Text( - firebaseService!.isEnabled - ? 'Firebase services are active' - : 'Firebase services are disabled', - ), - value: firebaseService!.isEnabled, - onChanged: null, // Read-only for now - ), - if (nostrService != null && syncEngine != null) ...[ - const Divider(), - ListTile( - leading: const Icon(Icons.cloud), - title: const Text('Relay Management'), - subtitle: const Text('Manage Nostr relays'), - trailing: const Icon(Icons.chevron_right), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => RelayManagementScreen( - controller: RelayManagementController( - nostrService: nostrService!, - syncEngine: syncEngine!, - ), - ), + Builder( + builder: (context) { + final firebaseService = ServiceLocator.instance.firebaseService; + if (firebaseService != null) { + return SwitchListTile( + title: const Text('Firebase Enabled'), + subtitle: Text( + firebaseService.isEnabled + ? 'Firebase services are active' + : 'Firebase services are disabled', ), + value: firebaseService.isEnabled, + onChanged: null, // Read-only for now + ); + } + return const SizedBox.shrink(); + }, + ), + Builder( + builder: (context) { + final nostrService = ServiceLocator.instance.nostrService; + final syncEngine = ServiceLocator.instance.syncEngine; + if (nostrService != null && syncEngine != null) { + return Column( + children: [ + const Divider(), + ListTile( + leading: const Icon(Icons.cloud), + title: const Text('Relay Management'), + subtitle: const Text('Manage Nostr relays'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => RelayManagementScreen( + controller: RelayManagementController( + nostrService: nostrService, + syncEngine: syncEngine, + ), + ), + ), + ); + }, + ), + ], ); - }, - ), - ], + } + return const SizedBox.shrink(); + }, + ), const Divider(), const ListTile( leading: Icon(Icons.info_outline), diff --git a/test/config/config_loader_test.dart b/test/config/config_loader_test.dart index ef1f5ac..e3ba320 100644 --- a/test/config/config_loader_test.dart +++ b/test/config/config_loader_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:app_boilerplate/core/exceptions/invalid_environment_exception.dart'; import 'package:app_boilerplate/config/config_loader.dart'; void main() { diff --git a/test/data/firebase/firebase_service_test.dart b/test/data/firebase/firebase_service_test.dart index b73dff0..712f3bb 100644 --- a/test/data/firebase/firebase_service_test.dart +++ b/test/data/firebase/firebase_service_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:app_boilerplate/core/exceptions/firebase_exception.dart' show FirebaseServiceException; import 'package:app_boilerplate/data/firebase/firebase_service.dart'; import 'package:app_boilerplate/data/firebase/models/firebase_config.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; @@ -81,7 +82,7 @@ void main() { // but that's expected - Firebase requires actual project setup expect( () => service.initialize(), - throwsA(isA()), + throwsA(isA()), ); }); }); @@ -102,7 +103,7 @@ void main() { email: 'test@example.com', password: 'password123', ), - throwsA(isA()), + throwsA(isA()), ); }); @@ -118,7 +119,7 @@ void main() { email: 'test@example.com', password: 'password123', ), - throwsA(isA()), + throwsA(isA()), ); }); @@ -134,7 +135,7 @@ void main() { expect( () => service.logout(), - throwsA(isA()), + throwsA(isA()), ); }); }); @@ -152,7 +153,7 @@ void main() { expect( () => service.syncItemsToFirestore('user1'), - throwsA(isA()), + throwsA(isA()), ); }); @@ -165,7 +166,7 @@ void main() { expect( () => service.syncItemsToFirestore('user1'), - throwsA(isA()), + throwsA(isA()), ); }); @@ -181,7 +182,7 @@ void main() { expect( () => service.syncItemsFromFirestore('user1'), - throwsA(isA()), + throwsA(isA()), ); }); @@ -194,7 +195,7 @@ void main() { expect( () => service.syncItemsFromFirestore('user1'), - throwsA(isA()), + throwsA(isA()), ); }); }); @@ -215,7 +216,7 @@ void main() { expect( () => service.uploadFile(testFile, 'test/path.txt'), - throwsA(isA()), + throwsA(isA()), ); }); @@ -231,7 +232,7 @@ void main() { expect( () => service.uploadFile(testFile, 'test/path.txt'), - throwsA(isA()), + throwsA(isA()), ); }); }); diff --git a/test/data/immich/immich_service_test.dart b/test/data/immich/immich_service_test.dart index 95fe3af..74938e6 100644 --- a/test/data/immich/immich_service_test.dart +++ b/test/data/immich/immich_service_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:dio/dio.dart'; +import 'package:app_boilerplate/core/exceptions/immich_exception.dart'; import 'package:app_boilerplate/data/immich/immich_service.dart'; import 'package:app_boilerplate/data/immich/models/immich_asset.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; diff --git a/test/data/nostr/nostr_service_test.dart b/test/data/nostr/nostr_service_test.dart index 81ee33f..ce074ee 100644 --- a/test/data/nostr/nostr_service_test.dart +++ b/test/data/nostr/nostr_service_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:app_boilerplate/core/exceptions/nostr_exception.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_event.dart'; diff --git a/test/data/session/session_service_test.dart b/test/data/session/session_service_test.dart index 6f06c15..333c761 100644 --- a/test/data/session/session_service_test.dart +++ b/test/data/session/session_service_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:app_boilerplate/core/exceptions/session_exception.dart'; import 'package:app_boilerplate/data/session/session_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/models/item.dart'; diff --git a/test/data/sync/sync_engine_test.dart b/test/data/sync/sync_engine_test.dart index f70a5c1..f6dd6fc 100644 --- a/test/data/sync/sync_engine_test.dart +++ b/test/data/sync/sync_engine_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:app_boilerplate/core/exceptions/sync_exception.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/sync/models/sync_status.dart'; import 'package:app_boilerplate/data/sync/models/sync_operation.dart'; diff --git a/test/ui/navigation/main_navigation_scaffold_test.dart b/test/ui/navigation/main_navigation_scaffold_test.dart index 9aac26a..5339d8b 100644 --- a/test/ui/navigation/main_navigation_scaffold_test.dart +++ b/test/ui/navigation/main_navigation_scaffold_test.dart @@ -9,6 +9,7 @@ import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/session/session_service.dart'; import 'package:app_boilerplate/data/firebase/firebase_service.dart'; +import 'package:app_boilerplate/core/service_locator.dart'; import 'main_navigation_scaffold_test.mocks.dart'; @@ -42,17 +43,25 @@ void main() { // Stub NostrService methods that might be called by UI final mockKeyPair = NostrKeyPair.generate(); when(mockNostrService.generateKeyPair()).thenReturn(mockKeyPair); + + // Register services with ServiceLocator + ServiceLocator.instance.registerServices( + localStorageService: mockLocalStorageService, + nostrService: mockNostrService, + syncEngine: mockSyncEngine, + sessionService: mockSessionService, + firebaseService: mockFirebaseService, + ); + }); + + tearDown(() { + // Reset ServiceLocator after each test + ServiceLocator.instance.reset(); }); Widget createTestWidget() { return MaterialApp( - home: MainNavigationScaffold( - sessionService: mockSessionService, - localStorageService: mockLocalStorageService, - nostrService: mockNostrService, - syncEngine: mockSyncEngine, - firebaseService: mockFirebaseService, - ), + home: const MainNavigationScaffold(), ); }