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 '../data/blossom/blossom_service.dart'; import '../data/media/media_service_interface.dart'; import '../data/media/multi_media_service.dart'; import '../data/media/models/media_server_config.dart'; import '../data/nostr/models/nostr_keypair.dart'; import '../data/recipes/recipe_service.dart'; import 'app_services.dart'; import 'service_locator.dart'; import 'logger.dart'; import 'theme_notifier.dart'; import 'package:flutter/material.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 MultiMediaService for managing multiple media servers with fallback Logger.debug('Initializing MultiMediaService...'); final multiMediaService = MultiMediaService(localStorage: storageService); await multiMediaService.loadServers(); // Migrate old single server config if no servers exist if (multiMediaService.getServers().isEmpty) { final settingsItem = await storageService.getItem('app_settings'); final immichEnabled = config.immichEnabled; final defaultBlossomServer = config.blossomServer; final mediaServerType = settingsItem?.data['media_server_type'] as String? ?? (immichEnabled ? 'immich' : 'blossom'); final immichBaseUrl = settingsItem?.data['immich_base_url'] as String? ?? config.immichBaseUrl; final immichApiKey = settingsItem?.data['immich_api_key'] as String? ?? config.immichApiKey; final blossomBaseUrl = settingsItem?.data['blossom_base_url'] as String? ?? defaultBlossomServer; // Migrate Immich config if (immichEnabled && mediaServerType == 'immich' && immichBaseUrl.isNotEmpty && immichApiKey.isNotEmpty) { final config = MediaServerConfig( id: 'immich-${DateTime.now().millisecondsSinceEpoch}', type: 'immich', baseUrl: immichBaseUrl, apiKey: immichApiKey, isDefault: true, name: 'Immich Server', ); await multiMediaService.addServer(config); Logger.info('Migrated Immich server config to MultiMediaService'); } // Migrate Blossom config if ((mediaServerType == 'blossom' || !immichEnabled) && blossomBaseUrl.isNotEmpty) { final config = MediaServerConfig( id: 'blossom-${DateTime.now().millisecondsSinceEpoch}', type: 'blossom', baseUrl: blossomBaseUrl, isDefault: true, name: 'Blossom Server', ); await multiMediaService.addServer(config); Logger.info('Migrated Blossom server config to MultiMediaService'); } await multiMediaService.saveServers(); } MediaServiceInterface? mediaService = multiMediaService; if (multiMediaService.getServers().isEmpty) { Logger.warning('No media servers configured'); mediaService = null; } else { Logger.info('MultiMediaService initialized with ${multiMediaService.getServers().length} server(s)'); } // 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'); // Initialize RecipeService Logger.debug('Initializing recipe service...'); final recipeService = RecipeService( localStorage: storageService, nostrService: nostrService, ); try { await recipeService.initialize(); Logger.info('Recipe service initialized'); } catch (e) { Logger.error('Failed to initialize recipe service: $e', e); // Continue without recipe service - it will be initialized later when user logs in } // Initialize ThemeNotifier and load theme preference Logger.debug('Initializing theme notifier...'); final themeNotifier = ThemeNotifier(); try { final settingsItem = await storageService.getItem('app_settings'); if (settingsItem != null && settingsItem.data.containsKey('dark_mode')) { final isDark = settingsItem.data['dark_mode'] == true; themeNotifier.setThemeMode(isDark ? ThemeMode.dark : ThemeMode.light); Logger.info('Theme preference loaded: ${isDark ? "dark" : "light"}'); } else { Logger.info('No theme preference found, using light mode'); } } catch (e) { Logger.warning('Failed to load theme preference: $e'); } // Create AppServices container final appServices = AppServices( localStorageService: storageService, nostrService: nostrService, syncEngine: syncEngine, firebaseService: firebaseService, sessionService: sessionService, mediaService: mediaService, recipeService: recipeService, ); // Register all services with ServiceLocator ServiceLocator.instance.registerServices( localStorageService: storageService, nostrService: nostrService, syncEngine: syncEngine, firebaseService: firebaseService, sessionService: sessionService, mediaService: mediaService, recipeService: recipeService, themeNotifier: themeNotifier, ); Logger.info('Application initialization completed successfully'); return appServices; } /// Reinitializes the media service based on current settings. /// /// This should be called when media server settings are changed. /// Preserves the Nostr keypair if Blossom is selected. static Future reinitializeMediaService() async { Logger.info('Reinitializing media service...'); final localStorage = ServiceLocator.instance.localStorageService; if (localStorage == null) { Logger.warning( 'Cannot reinitialize media service: LocalStorageService not available'); return; } // Create new MultiMediaService instance and load servers final multiMediaService = MultiMediaService(localStorage: localStorage); await multiMediaService.loadServers(); MediaServiceInterface? newMediaService = multiMediaService; if (multiMediaService.getServers().isEmpty) { Logger.warning('No media servers configured'); newMediaService = null; } else { Logger.info('MultiMediaService reinitialized with ${multiMediaService.getServers().length} server(s)'); } // Register the new media service with ServiceLocator ServiceLocator.instance.registerServices( mediaService: newMediaService, ); Logger.info('Media service reinitialized and registered successfully'); } }