diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart new file mode 100644 index 0000000..0628ab8 --- /dev/null +++ b/lib/core/app_initializer.dart @@ -0,0 +1,156 @@ +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 'app_services.dart'; +import 'service_locator.dart'; +import 'logger.dart'; + +/// Initializes all application services. +/// +/// Handles the complete initialization sequence: +/// 1. Load environment configuration +/// 2. Initialize Firebase (if enabled) +/// 3. Initialize local storage +/// 4. Initialize Nostr service and sync engine +/// 5. Initialize Immich service +/// 6. Initialize Firebase service (if enabled) +/// 7. Initialize session service +/// 8. Register all services with ServiceLocator +class AppInitializer { + /// Initializes all application services. + /// + /// [environment] - The environment to use ('dev' or 'prod'). + /// + /// Returns an [AppServices] instance with all initialized services. + /// + /// Throws if initialization fails. + static Future initialize({ + String environment = 'dev', + }) async { + Logger.info('Starting application initialization...'); + + // Load .env file (optional - falls back to defaults if not found) + try { + await dotenv.load(fileName: '.env'); + Logger.debug('.env file loaded successfully'); + } catch (e) { + Logger.warning('.env file not found, using default values: $e'); + } + + // Load configuration based on environment + final config = ConfigLoader.load(environment); + Logger.setEnabled(config.enableLogging); + Logger.info('Configuration loaded for environment: $environment'); + + // Initialize Firebase if enabled + if (config.firebaseConfig.enabled) { + try { + await Firebase.initializeApp(); + Logger.info('Firebase initialized successfully'); + } catch (e) { + Logger.error( + 'Firebase initialization failed: $e', + e, + ); + Logger.warning( + 'Note: Firebase requires google-services.json (Android) and GoogleService-Info.plist (iOS)', + ); + } + } + + // Initialize local storage service + Logger.debug('Initializing local storage service...'); + final storageService = LocalStorageService(); + try { + await storageService.initialize(); + Logger.info('Local storage service initialized'); + } catch (e) { + Logger.error('Failed to initialize storage: $e', e); + rethrow; + } + + // Initialize Nostr service and sync engine + Logger.debug('Initializing Nostr service...'); + final nostrService = NostrService(); + final nostrKeyPair = nostrService.generateKeyPair(); + + final syncEngine = SyncEngine( + localStorage: storageService, + nostrService: nostrService, + nostrKeyPair: nostrKeyPair, + ); + Logger.info('Nostr service and sync engine initialized'); + + // Load relays from config + for (final relayUrl in config.nostrRelays) { + nostrService.addRelay(relayUrl); + } + Logger.debug('Loaded ${config.nostrRelays.length} relay(s) from config'); + + // Initialize Immich service + Logger.debug('Initializing Immich service...'); + final immichService = ImmichService( + baseUrl: config.immichBaseUrl, + apiKey: config.immichApiKey, + localStorage: storageService, + ); + Logger.info('Immich service initialized'); + + // Initialize Firebase service if enabled + FirebaseService? firebaseService; + if (config.firebaseConfig.enabled) { + try { + Logger.debug('Initializing Firebase service...'); + firebaseService = FirebaseService( + config: config.firebaseConfig, + localStorage: storageService, + ); + await firebaseService.initialize(); + Logger.info('Firebase service initialized: ${firebaseService.isEnabled}'); + } catch (e) { + Logger.error('Firebase service initialization failed: $e', e); + firebaseService = null; + } + } + + // Initialize SessionService with Firebase and Nostr integration + Logger.debug('Initializing session service...'); + final sessionService = SessionService( + localStorage: storageService, + syncEngine: syncEngine, + firebaseService: firebaseService, + nostrService: nostrService, + ); + Logger.info('Session service initialized'); + + // Create AppServices container + final appServices = AppServices( + localStorageService: storageService, + nostrService: nostrService, + syncEngine: syncEngine, + firebaseService: firebaseService, + sessionService: sessionService, + immichService: immichService, + ); + + // Register all services with ServiceLocator + ServiceLocator.instance.registerServices( + localStorageService: storageService, + nostrService: nostrService, + syncEngine: syncEngine, + firebaseService: firebaseService, + sessionService: sessionService, + immichService: immichService, + ); + + Logger.info('Application initialization completed successfully'); + return appServices; + } +} + diff --git a/lib/core/app_services.dart b/lib/core/app_services.dart new file mode 100644 index 0000000..055e004 --- /dev/null +++ b/lib/core/app_services.dart @@ -0,0 +1,55 @@ +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'; + +/// Container for all application services. +/// +/// Holds references to all initialized services and provides +/// a convenient way to dispose of them. +class AppServices { + /// Local storage service. + final LocalStorageService localStorageService; + + /// Nostr service. + final NostrService? nostrService; + + /// Sync engine. + final SyncEngine? syncEngine; + + /// Firebase service. + final FirebaseService? firebaseService; + + /// Session service. + final SessionService? sessionService; + + /// Immich service. + final ImmichService? immichService; + + /// Creates an [AppServices] instance. + AppServices({ + required this.localStorageService, + this.nostrService, + this.syncEngine, + this.firebaseService, + this.sessionService, + this.immichService, + }); + + /// Disposes of all services that need cleanup. + Future dispose() async { + syncEngine?.dispose(); + nostrService?.dispose(); + firebaseService?.dispose(); + + // Close storage service + try { + await localStorageService.close(); + } catch (e) { + // Ignore errors during cleanup + } + } +} + diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..08b448d --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,24 @@ +/// Application-wide constants. +class AppConstants { + /// Application name. + static const String appName = 'App Boilerplate'; + + /// Default connection timeout duration. + static const Duration connectionTimeout = Duration(seconds: 3); + + /// Default health check timeout duration. + static const Duration healthCheckTimeout = Duration(seconds: 2); + + /// Default retry delay for operations. + static const Duration retryDelay = Duration(seconds: 1); + + /// Maximum number of retries for failed operations. + static const int maxRetries = 3; + + /// Maximum queue size for sync operations. + static const int maxQueueSize = 100; + + /// Private constructor to prevent instantiation. + AppConstants._(); +} + diff --git a/lib/core/constants/route_names.dart b/lib/core/constants/route_names.dart new file mode 100644 index 0000000..285d75c --- /dev/null +++ b/lib/core/constants/route_names.dart @@ -0,0 +1,24 @@ +/// Route names for navigation. +class RouteNames { + /// Home route. + static const String home = '/home'; + + /// Immich route. + static const String immich = '/immich'; + + /// Nostr Events route. + static const String nostrEvents = '/nostr-events'; + + /// Session route. + static const String session = '/session'; + + /// Settings route. + static const String settings = '/settings'; + + /// Relay Management route. + static const String relayManagement = '/relay-management'; + + /// Private constructor to prevent instantiation. + RouteNames._(); +} + diff --git a/lib/core/exceptions/exceptions.dart b/lib/core/exceptions/exceptions.dart new file mode 100644 index 0000000..dbccf0f --- /dev/null +++ b/lib/core/exceptions/exceptions.dart @@ -0,0 +1,10 @@ +/// Barrel file for all exceptions. +/// +/// Import this file to get access to all exception classes. +export 'firebase_exception.dart' show FirebaseServiceException; +export 'immich_exception.dart'; +export 'invalid_environment_exception.dart'; +export 'nostr_exception.dart'; +export 'session_exception.dart'; +export 'sync_exception.dart'; + diff --git a/lib/core/exceptions/firebase_exception.dart b/lib/core/exceptions/firebase_exception.dart new file mode 100644 index 0000000..48c5609 --- /dev/null +++ b/lib/core/exceptions/firebase_exception.dart @@ -0,0 +1,15 @@ +/// Exception thrown when Firebase service operations fail. +/// +/// Note: This is different from Firebase SDK's FirebaseException. +/// This exception is used for our FirebaseService wrapper. +class FirebaseServiceException implements Exception { + /// Error message. + final String message; + + /// Creates a [FirebaseServiceException] with the provided message. + FirebaseServiceException(this.message); + + @override + String toString() => 'FirebaseServiceException: $message'; +} + diff --git a/lib/core/exceptions/immich_exception.dart b/lib/core/exceptions/immich_exception.dart new file mode 100644 index 0000000..fb0b898 --- /dev/null +++ b/lib/core/exceptions/immich_exception.dart @@ -0,0 +1,20 @@ +/// 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'; + } +} + diff --git a/lib/core/exceptions/invalid_environment_exception.dart b/lib/core/exceptions/invalid_environment_exception.dart new file mode 100644 index 0000000..37d82dd --- /dev/null +++ b/lib/core/exceptions/invalid_environment_exception.dart @@ -0,0 +1,15 @@ +/// 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'; + } +} + diff --git a/lib/core/exceptions/nostr_exception.dart b/lib/core/exceptions/nostr_exception.dart new file mode 100644 index 0000000..2b4405e --- /dev/null +++ b/lib/core/exceptions/nostr_exception.dart @@ -0,0 +1,12 @@ +/// 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'; +} + diff --git a/lib/core/exceptions/session_exception.dart b/lib/core/exceptions/session_exception.dart new file mode 100644 index 0000000..28a9a0e --- /dev/null +++ b/lib/core/exceptions/session_exception.dart @@ -0,0 +1,12 @@ +/// 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'; +} + diff --git a/lib/core/exceptions/sync_exception.dart b/lib/core/exceptions/sync_exception.dart new file mode 100644 index 0000000..4fbf26a --- /dev/null +++ b/lib/core/exceptions/sync_exception.dart @@ -0,0 +1,12 @@ +/// 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'; +} + diff --git a/lib/core/logger.dart b/lib/core/logger.dart new file mode 100644 index 0000000..7b3bd90 --- /dev/null +++ b/lib/core/logger.dart @@ -0,0 +1,82 @@ +import 'package:flutter/foundation.dart'; + +/// Log levels for different types of messages. +enum LogLevel { + /// Debug messages (only in debug mode). + debug, + + /// Informational messages. + info, + + /// Warning messages. + warning, + + /// Error messages (always shown). + error, +} + +/// Centralized logging service for the application. +/// +/// Provides consistent logging across the app with different log levels. +/// In production, only errors are logged unless explicitly enabled. +class Logger { + /// Whether logging is enabled (can be configured via config). + static bool _enabled = kDebugMode; + + /// Enable or disable logging. + static void setEnabled(bool enabled) { + _enabled = enabled; + } + + /// Logs a message with the specified level. + /// + /// [level] - The log level (debug, info, warning, error). + /// [message] - The message to log. + /// [error] - Optional error object. + /// [stackTrace] - Optional stack trace. + static void log( + LogLevel level, + String message, [ + Object? error, + StackTrace? stackTrace, + ]) { + // Always log errors, respect enabled flag for others + if (!_enabled && level != LogLevel.error) { + return; + } + + final prefix = '[${level.name.toUpperCase()}]'; + final timestamp = DateTime.now().toIso8601String(); + + debugPrint('$timestamp $prefix $message'); + + if (error != null) { + debugPrint('$timestamp $prefix Error: $error'); + } + + if (stackTrace != null) { + debugPrint('$timestamp $prefix Stack trace: $stackTrace'); + } + } + + /// Logs a debug message (only in debug mode). + static void debug(String message) { + log(LogLevel.debug, message); + } + + /// Logs an informational message. + static void info(String message) { + log(LogLevel.info, message); + } + + /// Logs a warning message. + static void warning(String message, [Object? error]) { + log(LogLevel.warning, message, error); + } + + /// Logs an error message (always shown). + static void error(String message, [Object? error, StackTrace? stackTrace]) { + log(LogLevel.error, message, error, stackTrace); + } +} + diff --git a/lib/core/service_locator.dart b/lib/core/service_locator.dart new file mode 100644 index 0000000..298d7d7 --- /dev/null +++ b/lib/core/service_locator.dart @@ -0,0 +1,85 @@ +/// Service locator for dependency injection. +/// +/// Provides a centralized registry for all application services, +/// making it easy to access services throughout the app +/// and swap implementations for testing. +class ServiceLocator { + /// Private constructor to prevent instantiation. + ServiceLocator._(); + + /// Singleton instance. + static final ServiceLocator instance = ServiceLocator._(); + + /// Local storage service. + dynamic _localStorageService; + + /// Nostr service. + dynamic _nostrService; + + /// Sync engine. + dynamic _syncEngine; + + /// Firebase service. + dynamic _firebaseService; + + /// Session service. + dynamic _sessionService; + + /// Immich service. + dynamic _immichService; + + /// Registers all services with the locator. + /// + /// All services are optional and can be null if not configured. + void registerServices({ + dynamic localStorageService, + dynamic nostrService, + dynamic syncEngine, + dynamic firebaseService, + dynamic sessionService, + dynamic immichService, + }) { + _localStorageService = localStorageService; + _nostrService = nostrService; + _syncEngine = syncEngine; + _firebaseService = firebaseService; + _sessionService = sessionService; + _immichService = immichService; + } + + /// Gets the local storage service. + /// + /// Throws [StateError] if service is not registered. + dynamic get localStorageService { + if (_localStorageService == null) { + throw StateError('LocalStorageService not registered'); + } + return _localStorageService; + } + + /// Gets the Nostr service (nullable). + dynamic get nostrService => _nostrService; + + /// Gets the sync engine (nullable). + dynamic get syncEngine => _syncEngine; + + /// Gets the Firebase service (nullable). + dynamic get firebaseService => _firebaseService; + + /// Gets the session service (nullable). + dynamic get sessionService => _sessionService; + + /// Gets the Immich service (nullable). + dynamic get immichService => _immichService; + + /// Clears all registered services (useful for testing). + void reset() { + _localStorageService = null; + _nostrService = null; + _syncEngine = null; + _firebaseService = null; + _sessionService = null; + _immichService = null; + } +} +