import 'dart:async'; import 'dart:io'; import 'dart:isolate'; import 'dart:convert'; import 'package:country_codes/country_codes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart' as http; import 'package:intl/date_symbol_data_local.dart'; import 'package:search_engine/database.dart'; import 'package:search_engine/widgets/navigation_bar.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:search_engine/services/config_service.dart'; import 'package:search_engine/controllers/notification_controller.dart'; import 'package:search_engine/services/mimir_service.dart'; import 'package:search_engine/services/live_activities_service.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LandingPage extends StatefulWidget { final bool? changeLocale; const LandingPage({super.key, this.changeLocale}); @override // ignore: library_private_types_in_public_api _LandingPageState createState() => _LandingPageState(); } class _LandingPageState extends State { double _updateProgress = 0.0; late Future _databaseFuture; String _updateInfo = 'downloading_data'.tr(); final _baseUrl = dotenv.env['BASE_URL']; final _token = dotenv.env['TOKEN']; final _version = dotenv.env['VERSION'] ?? '1.0'; // Tamaño del lote para inserción en la base de datos static const int _batchSize = 50; final _mimirService = MimirService(); final _liveActivitiesService = LiveActivitiesService(); String? _syncActivityId; @override void initState() { super.initState(); _databaseFuture = Future.value(AppDatabase()); _mimirService.initialize(); // Initialize Live Activities first, then continue with fetchData _initLiveActivities().then((_) { fetchData(); }); // Initialize notifications NotificationController.initialize().then((_) { NotificationController.requestPermissions(); }); } @override void dispose() { _databaseFuture.then((database) => database.close()); if (_syncActivityId != null) { _liveActivitiesService.endSearchActivity(_syncActivityId!); } super.dispose(); } void _goHome() { if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const GlobalNavigator())); } } Future fetchData() async { if (kDebugMode) print('🔄 Iniciando fetchData'); final database = AppDatabase(); try { // Check if SharedPreferences is working correctly if (kDebugMode) { try { final prefs = await SharedPreferences.getInstance(); final prefsKeys = prefs.getKeys(); print('🔑 SharedPreferences available keys: $prefsKeys'); } catch (e) { print('⚠️ Cannot access SharedPreferences: $e'); } } final locale = await getLocale(); if (kDebugMode) print('🌍 Locale obtenido: $locale'); var date = await getDate(); if (kDebugMode) { print('📅 Fecha última sincronización: $date'); try { final parsedDate = DateTime.parse(date); final now = DateTime.now(); if (parsedDate.isAfter(now)) { print('⚠️ WARNING: Last sync date is in the future!'); print('📅 Future date: $parsedDate, Current date: $now'); } } catch (e) { print('⚠️ Error parsing date: $e'); } } setState(() { _updateInfo = 'connecting_to_server'.tr(); _updateProgress = 0.0; }); // Create search activity only if on iOS and LiveActivities were initialized properly if (Platform.isIOS) { _syncActivityId = await _createLiveActivity( 'sync_title'.tr(), 'sync_in_progress'.tr(), '0', ); } // Verificar conectividad de forma más rápida bool isConnected = false; try { final testUrl = Uri.parse('$_baseUrl/server/ping?access_token=$_token'); final testResponse = await http.get(testUrl).timeout( const Duration(seconds: 3), onTimeout: () { throw TimeoutException('No se pudo conectar al servidor'); }, ); isConnected = testResponse.statusCode == 200; } catch (e) { if (kDebugMode) print('❌ Error de conectividad: $e'); } if (!isConnected) { setState(() { _updateInfo = 'server_unreachable'.tr(); }); await Future.delayed(const Duration(seconds: 1)); database.close(); if (_syncActivityId != null) { await _liveActivitiesService.endSearchActivity(_syncActivityId!); } _goHome(); return; } // Try to get the most recent item date from server to compare final serverLatestDate = await _getServerLatestDate(locale); if (kDebugMode) { print('📅 Server latest date: $serverLatestDate'); // Parse dates for comparison DateTime? localDate; DateTime? serverDate; try { localDate = DateTime.parse(date); if (serverLatestDate != null) { serverDate = DateTime.parse(serverLatestDate); } } catch (e) { print('⚠️ Date parsing error: $e'); } // Compare dates if both are valid if (localDate != null && serverDate != null) { final difference = serverDate.difference(localDate).inDays; print('📊 Days difference between local and server: $difference'); if (difference <= 0) { print('✅ Local date is up-to-date or ahead of server'); } else { print('⚠️ Server has $difference days of newer data'); } } } // Obtener el total de elementos de forma más eficiente final totalItems = await _getTotalItemsCount(locale, date); if (totalItems == 0) { if (kDebugMode) print('ℹ️ No hay elementos nuevos'); setState(() { _updateInfo = 'no_new_data'.tr(); }); await Future.delayed(const Duration(seconds: 1)); database.close(); if (_syncActivityId != null) { await _liveActivitiesService.endSearchActivity(_syncActivityId!); } _goHome(); return; } if (_syncActivityId != null && Platform.isIOS && _liveActivitiesService.isSupported) { try { await _liveActivitiesService.updateSearchActivity( activityId: _syncActivityId!, title: 'sync_title'.tr(), query: 'processing_data'.tr(), count: totalItems.toString(), ); } catch (e) { if (kDebugMode) { print('Error updating Live Activity: $e'); } } } // Inicializar contadores int processedItems = 0; int currentPage = 1; bool hasMoreData = true; final int pageSize = 50; DateTime? lastProcessedDate; // Variable para almacenar la fecha más reciente global DateTime? mostRecentDateGlobal; setState(() { _updateInfo = 'processing_data'.tr(); }); while (hasMoreData) { try { if (kDebugMode) print('📑 Procesando página $currentPage'); // Ensure the date is properly formatted and encoded for URL final encodedDate = Uri.encodeComponent(date); if (kDebugMode) print('🔗 URL date parameter: $encodedDate'); final url = Uri.parse( '$_baseUrl/items/activities_translations?sort=-activities_id.date&fields=*,activities_id.*,interventions.text&filter[languages_code][_eq]=$locale&filter[_or][0][activities_id][date_updated][_gt]=$encodedDate&filter[_or][1][activities_id][date_created][_gt]=$encodedDate&limit=$pageSize&page=$currentPage&access_token=$_token'); final response = await http.get(url).timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { throw HttpException( 'Error en página $currentPage: ${response.statusCode}'); } final jsonResponse = jsonDecode(utf8.decode(response.bodyBytes)); final List data = jsonResponse['data'] ?? []; if (data.isEmpty) { hasMoreData = false; continue; } // Procesar datos en un isolate final processedData = await compute( _processBatchItemsIsolate, data, ); final List batchMessages = processedData['messages']; final List> mimirDocuments = processedData['mimirDocuments']; // Guardar solo la información general en la base de datos if (batchMessages.isNotEmpty) { try { await database.addMessages(batchMessages); processedItems += batchMessages.length; // Guardar documentos en Mimir if (mimirDocuments.isNotEmpty) { await _mimirService.addDocuments(mimirDocuments); } // Actualizar la fecha del último registro procesado final firstItemDate = batchMessages.first.date; if (firstItemDate != null) { // Usar directamente la fecha del primer elemento procesado (el más reciente) lastProcessedDate = firstItemDate; // Actualizar la fecha global más reciente si es necesario if (mostRecentDateGlobal == null || firstItemDate.isAfter(mostRecentDateGlobal!)) { mostRecentDateGlobal = firstItemDate; if (kDebugMode) { print( '📅 Nueva fecha global más reciente: ${DateFormat('yyyy-MM-ddTHH:mm:ss').format(mostRecentDateGlobal!)}'); } } if (kDebugMode) { print( '📅 Fecha del elemento más reciente procesado: ${DateFormat('yyyy-MM-ddTHH:mm:ss').format(lastProcessedDate)}'); } // Guardar fecha cada 5 lotes o al final if (currentPage % 5 == 0 || !hasMoreData) { // Guardar la fecha más reciente global, no la del último lote procesado await setDate(mostRecentDateGlobal ?? lastProcessedDate); } } setState(() { _updateProgress = processedItems / totalItems; _updateInfo = 'processed_items' .tr(args: ['$processedItems', '$totalItems']); }); if (_syncActivityId != null && Platform.isIOS && _liveActivitiesService.isSupported) { try { await _liveActivitiesService.updateSearchActivity( activityId: _syncActivityId!, title: 'sync_title'.tr(), query: 'processed_items' .tr(args: ['$processedItems', '$totalItems']), count: processedItems.toString(), ); } catch (e) { if (kDebugMode) { print('Error updating Live Activity: $e'); } } } } catch (e) { if (kDebugMode) print('❌ Error guardando lote: $e'); // Guardar fecha en caso de error if (mostRecentDateGlobal != null) { await setDate(mostRecentDateGlobal); } else if (lastProcessedDate != null) { await setDate(lastProcessedDate); } continue; } } currentPage++; } catch (e) { if (kDebugMode) print('❌ Error procesando página $currentPage: $e'); // Guardar fecha en caso de error if (mostRecentDateGlobal != null) { await setDate(mostRecentDateGlobal); } else if (lastProcessedDate != null) { await setDate(lastProcessedDate); } currentPage++; if (currentPage > (totalItems / pageSize) + 2) { hasMoreData = false; } } } if (processedItems > 0) { // Asegurarnos de que estamos guardando la fecha más reciente global final DateTime finalDate = mostRecentDateGlobal ?? lastProcessedDate ?? DateTime.now(); final String formattedFinalDate = DateFormat('yyyy-MM-ddTHH:mm:ss').format(finalDate); if (kDebugMode) { print( '📅 Fecha final de procesamiento (más reciente global): $formattedFinalDate'); } // Call the new function to finalize the sync process await _finalizeSyncProcess( processedItems, totalItems, mostRecentDateGlobal); } if (_syncActivityId != null && Platform.isIOS && _liveActivitiesService.isSupported) { try { await _liveActivitiesService.endSearchActivity(_syncActivityId!); _syncActivityId = null; } catch (e) { if (kDebugMode) { print('Error ending Live Activity: $e'); } } } database.close(); _goHome(); } catch (e) { if (kDebugMode) print('❌ Error general: $e'); setState(() { _updateInfo = 'sync_error'.tr(); }); if (_syncActivityId != null && Platform.isIOS && _liveActivitiesService.isSupported) { try { await _liveActivitiesService.updateSearchActivity( activityId: _syncActivityId!, title: 'sync_error'.tr(), query: e.toString(), count: '0', ); await _liveActivitiesService.endSearchActivity(_syncActivityId!); _syncActivityId = null; } catch (e) { if (kDebugMode) { print('Error updating Live Activity: $e'); } } } await Future.delayed(const Duration(seconds: 2)); _goHome(); } } // Método para obtener el total de elementos optimizado Future _getTotalItemsCount(String locale, String date) async { try { final countUrl = Uri.parse( '$_baseUrl/items/activities_translations?filter[languages_code][_eq]=$locale&filter[_or][0][activities_id][date_updated][_gt]=$date&filter[_or][1][activities_id][date_created][_gt]=$date&aggregate[count]=*&access_token=$_token'); final countResponse = await http.get(countUrl).timeout( const Duration(seconds: 5), ); if (countResponse.statusCode == 200) { final countJson = jsonDecode(utf8.decode(countResponse.bodyBytes)); if (countJson['data'] != null && countJson['data'] is List && countJson['data'].isNotEmpty) { final totalItems = countJson['data'][0]['count'] ?? 0; if (kDebugMode) print('📈 Total de elementos: $totalItems'); return totalItems; } } } catch (e) { if (kDebugMode) print('⚠️ Error obteniendo total de elementos: $e'); } return 0; } // Función estática para procesar lotes de elementos en un isolate static Future> _processBatchItemsIsolate( List data) async { final List batchMessages = []; final List> mimirDocuments = []; for (final item in data) { try { final draft = _processDraftItemIsolate(item); if (draft != null) { // Guardar solo la información general en la base de datos batchMessages.add(draft); // Guardar el contenido completo en Mimir if (draft.body != null && draft.body!.isNotEmpty) { mimirDocuments.add({ 'id': draft.id, 'languages_code': draft.languagesCode, 'content': draft.body!, }); } } } catch (e) { if (kDebugMode) print('⚠️ Error procesando item en isolate: $e'); continue; } } return { 'messages': batchMessages, 'mimirDocuments': mimirDocuments, }; } // Versión estática del método _processDraftItem para usar en isolates static Draft? _processDraftItemIsolate(Map item) { final String id = _safeStringStatic(item['activities_id']?['id']); if (id.isEmpty) return null; // Manejar el caso cuando interventions está vacío String body = ''; if (item['interventions'] != null && item['interventions'] is List && (item['interventions'] as List).isNotEmpty) { body = _safeStringStatic(item['interventions'][0]['text']); } return Draft( id: id, title: _safeStringStatic(item['title']), pdf: _safeStringStatic(item['pdf']), date: _parseDate(item['activities_id']?['date']), body: body, activity: item['activities_id']?['activity'] is int ? item['activities_id']['activity'] : 1, country: _safeStringStatic(item['activities_id']?['country']), city: _safeStringStatic(item['activities_id']?['city']), thumbnail: _safeStringStatic(item['activities_id']?['thumbnail']), draft: item['activities_id']?['draft'] == true ? 1 : 0, locale: _safeStringStatic(item['languages_code']), languagesCode: _safeStringStatic(item['languages_code'])); } // Versión estática de _safeString para usar en isolates static String _safeStringStatic(dynamic value) { if (value == null) return ''; return value.toString(); } // Versión estática del método _parseDate para usar en isolates static DateTime _parseDate(String? dateStr) { if (dateStr == null) return DateTime.now(); try { return DateTime.parse(dateStr); } catch (e) { return DateTime.now(); } } @override Widget build(BuildContext context) { return Scaffold( body: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFFffffff), Color(0xFFe3ead6)], begin: Alignment.topRight, end: Alignment.bottomLeft)), child: SafeArea( child: Padding( padding: const EdgeInsets.all(32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitle(), _buildVersion(), const SizedBox(height: 20), // Mostrar información de actualización AnimatedOpacity( opacity: _updateInfo.isNotEmpty ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Text( _updateInfo, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 8), // Mostrar progreso según el estado if (_updateProgress > 0 && _updateProgress < 1) // Barra de progreso determinado (cuando hay algo que procesar) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${(_updateProgress * 100).floor()}%', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0XFF6b8e23), ), ), const SizedBox(height: 4), TweenAnimationBuilder( tween: Tween( begin: 0, end: _updateProgress), duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, builder: (context, value, child) { return ClipRRect( borderRadius: BorderRadius.circular(10), child: LinearProgressIndicator( value: value, color: const Color(0XFF6b8e23), backgroundColor: const Color(0xFFe5ebd8), minHeight: 4, ), ); }, ), ], ) else if (_updateProgress == 0 && _updateInfo.isNotEmpty) // Indicador de progreso indeterminado (durante verificaciones) ClipRRect( borderRadius: BorderRadius.circular(56), child: const LinearProgressIndicator( value: null, // Modo indeterminado color: Color(0XFF6b8e23), backgroundColor: Color(0xFFe5ebd8), minHeight: 4, ), ), ])), // Logo con animación sutil AnimatedContainer( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, padding: const EdgeInsets.all(24), child: Center( child: _buildLogo(), ), ) ]))))); } Widget _buildTitle() { return Text('title'.tr(), style: const TextStyle( fontSize: 48, fontWeight: FontWeight.bold, color: Color(0xFF333333), height: 1)); } Widget _buildVersion() { return Text('${'version'.tr()} $_version', style: const TextStyle( fontSize: 20, color: Color(0xFF666666), fontWeight: FontWeight.w300, letterSpacing: 0.5)); } Widget _buildLogo() { return SvgPicture.asset('assets/svg/logo.svg', height: MediaQuery.of(context).size.height * 0.12, width: MediaQuery.of(context).size.width * 0.3, semanticsLabel: 'Logo LGCC'); } Future _showSyncNotification(String message) async { await NotificationController.showNotification( title: 'Data Sync', body: message, payload: 'sync_status', ); } // Método para sincronizar Mimir con la base de datos en un Isolate Future _syncMimirWithDatabase(String locale) async { final token = RootIsolateToken.instance; if (token == null) { throw Exception('RootIsolateToken is not initialized'); } setState(() { _updateInfo = 'syncing_search_index'.tr(); _updateProgress = 0.0; }); // Ejecutar la sincronización en un Isolate final syncResult = await compute( _syncMimirWithDatabaseIsolate, [locale, token], ); // Actualizar la UI con el progreso if (syncResult['success']) { setState(() { _updateProgress = 1.0; _updateInfo = 'search_index_updated'.tr(); }); await Future.delayed(const Duration(milliseconds: 500)); } else { if (kDebugMode) { print('❌ Error en sincronización: ${syncResult['error']}'); } } } // Función estática para ejecutar en un Isolate - Verificar estadísticas de DB y Mimir static Future> _checkDatabaseStatsIsolate( List params) async { final locale = params[0] as String; final token = params[1] as RootIsolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(token); final database = AppDatabase(); final mimirService = MimirService(); try { final mimirCount = await mimirService.getDocumentCount(); final dbMessages = await database.getAllMessages(locale); return { 'mimirCount': mimirCount, 'dbCount': dbMessages.length, }; } catch (e) { if (kDebugMode) { print('Error en _checkDatabaseStatsIsolate: $e'); } return { 'mimirCount': 0, 'dbCount': 0, 'error': e.toString(), }; } finally { database.close(); } } // Función estática para ejecutar en un Isolate - Sincronizar Mimir con la base de datos static Future> _syncMimirWithDatabaseIsolate( List params) async { final locale = params[0] as String; final token = params[1] as RootIsolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(token); final database = AppDatabase(); final mimirService = MimirService(); try { // Obtener todos los documentos de Mimir final mimirDocuments = await mimirService.getAllDocuments(); final mimirCount = mimirDocuments.length; // Crear un conjunto de IDs de documentos que ya están en Mimir final mimirIds = Set.from(mimirDocuments.map((doc) => doc['id'] as String)); // Obtener todos los mensajes de la base de datos final messages = await database.getAllMessages(locale); final dbCount = messages.length; if (kDebugMode) { print('📊 Documentos en Mimir: $mimirCount'); print('📊 Documentos en Base de Datos: $dbCount'); } // Encontrar documentos que necesitan ser añadidos (en DB pero no en Mimir) final documentsToAdd = messages.where((draft) { return !mimirIds.contains(draft.id) && draft.body != null && draft.body!.isNotEmpty; }).toList(); if (documentsToAdd.isNotEmpty) { if (kDebugMode) { print('🔄 Documentos a agregar a Mimir: ${documentsToAdd.length}'); } // Preparar documentos para Mimir final documents = documentsToAdd.map((draft) { return { 'id': draft.id, 'languages_code': draft.languagesCode, 'content': draft.body!, }; }).toList(); if (kDebugMode) { print('📝 Documentos válidos para indexar: ${documents.length}'); } // Añadir documentos a Mimir en lotes const batchSize = 50; for (var i = 0; i < documents.length; i += batchSize) { final end = (i + batchSize < documents.length) ? i + batchSize : documents.length; final batch = documents.sublist(i, end); await mimirService.addDocuments(batch); if (kDebugMode) { print( '✓ Progreso de indexación: ${i + batch.length}/${documents.length}'); } } if (kDebugMode) { final finalCount = await mimirService.getDocumentCount(); print('✅ Indexación completada. Documentos en Mimir: $finalCount'); } } else { if (kDebugMode) { print('✅ Mimir y Base de Datos están sincronizados'); } } return {'success': true}; } catch (e) { if (kDebugMode) { print('Error en _syncMimirWithDatabaseIsolate: $e'); } return { 'success': false, 'error': e.toString(), }; } finally { database.close(); } } Future getDate() async { final locale = await ConfigService.getLocale(); try { // Get the stored date from ConfigService final storedDateStr = await ConfigService.getLastDate(locale); if (kDebugMode) { print('📅 Raw stored date: $storedDateStr'); } // Default to a very old date if stored date is empty or "0" if (storedDateStr == '0' || storedDateStr.isEmpty) { const defaultDate = '2000-01-01T00:00:00'; if (kDebugMode) { print('📅 Using default old date: $defaultDate'); } return defaultDate; } // Check if the stored date is valid try { final storedDate = DateTime.parse(storedDateStr); final now = DateTime.now(); // If the stored date is in the future, return yesterday's date instead if (storedDate.isAfter(now)) { final yesterday = now.subtract(const Duration(days: 1)); final yesterdayStr = DateFormat('yyyy-MM-ddTHH:mm:ss').format(yesterday); if (kDebugMode) { print('⚠️ WARNING: Stored date ($storedDateStr) is in the future!'); print('📅 Using yesterday\'s date instead: $yesterdayStr'); } // Save the corrected date for future use await ConfigService.setLastDate(locale, yesterdayStr); return yesterdayStr; } // Date is valid and not in the future if (kDebugMode) { print('📅 Using stored date: $storedDateStr'); } return storedDateStr; } catch (e) { // Invalid date format, use a very old date const defaultDate = '2000-01-01T00:00:00'; if (kDebugMode) { print('⚠️ Error parsing date "$storedDateStr": $e'); print('📅 Using default old date: $defaultDate'); } return defaultDate; } } catch (e) { // Error retrieving date from ConfigService, use a very old date const defaultDate = '2000-01-01T00:00:00'; if (kDebugMode) { print('❌ Error retrieving date: $e'); print('📅 Using default old date: $defaultDate'); } return defaultDate; } } Future setDate([DateTime? specificDate]) async { // Get the current date final DateTime now = DateTime.now(); // Decide which date to use: // 1. If specificDate is provided and not in the future, use it // 2. Otherwise, use the current date DateTime dateToUse = now; if (specificDate != null) { if (specificDate.isAfter(now)) { if (kDebugMode) { print( '⚠️ WARNING: Specified date (${specificDate.toIso8601String()}) is in the future!'); print('📅 Using current date instead: ${now.toIso8601String()}'); } } else { dateToUse = specificDate; } } // Format the date final String formattedDate = DateFormat('yyyy-MM-ddTHH:mm:ss').format(dateToUse); try { // Save the date final locale = await ConfigService.getLocale(); await ConfigService.setLastDate(locale, formattedDate); if (kDebugMode) { print('💾 Saved date: $formattedDate for locale: $locale'); } // Verify the saved date final savedDate = await ConfigService.getLastDate(locale); if (kDebugMode) { if (savedDate != formattedDate) { print( '⚠️ WARNING: Verification failed - expected: $formattedDate, got: $savedDate'); } else { print('✅ Date verification successful: $savedDate'); } } return formattedDate; } catch (e) { if (kDebugMode) { print('❌ Error saving date: $e'); } return formattedDate; } } Future getLocale() async { final locale = await ConfigService.getLocale(); initializeDateFormatting(locale); return locale; } // Helper method to get the latest date from server Future _getServerLatestDate(String locale) async { try { final url = Uri.parse( '$_baseUrl/items/activities_translations?sort=-activities_id.date&fields=activities_id.date&filter[languages_code][_eq]=$locale&limit=1&access_token=$_token'); final response = await http.get(url).timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final jsonResponse = jsonDecode(utf8.decode(response.bodyBytes)); final List data = jsonResponse['data'] ?? []; if (data.isNotEmpty && data[0]['activities_id'] != null) { return data[0]['activities_id']['date']; } } } catch (e) { if (kDebugMode) { print('❌ Error getting server latest date: $e'); } } return null; } Future _initLiveActivities() async { if (Platform.isIOS) { if (kDebugMode) { print("🔄 Initializing Live Activities service"); } // If not on iOS, don't initialize LiveActivities // Initialize LiveActivities with the app group const String appGroupId = 'group.com.carpa.searchEngine.widget'; const String urlScheme = 'searchengine://livesearch'; if (kDebugMode) { print("📱 Using app group: $appGroupId"); print("🔗 Using URL scheme: $urlScheme"); } // Initialize Live Activities final bool isInitialized = await _liveActivitiesService.init( appGroupId: appGroupId, urlScheme: urlScheme, ); // Check if the initialization was successful if (kDebugMode) { print("LiveActivities initialized: $isInitialized"); print( "LiveActivities supported: ${_liveActivitiesService.isSupported}"); print( "LiveActivities appGroupId: ${_liveActivitiesService.appGroupId}"); } // Request permissions for notifications to ensure we can show Live Activities NotificationController.requestPermissions(); } } Future _finalizeSyncProcess(int processedItems, int totalItems, DateTime? mostRecentDateGlobal) async { if (processedItems > 0) { // Always use the current date for the final update final DateTime currentDate = DateTime.now(); final String formattedCurrentDate = DateFormat('yyyy-MM-ddTHH:mm:ss').format(currentDate); if (kDebugMode) { print('📅 Using current date for final update: $formattedCurrentDate'); if (mostRecentDateGlobal != null) { print( '📅 Most recent item date was: ${DateFormat('yyyy-MM-ddTHH:mm:ss').format(mostRecentDateGlobal)}'); } } // Save the current date await setDate(currentDate); // Verify that the date was saved correctly final savedDate = await getDate(); if (kDebugMode) { print('🔍 Verification - Current saved date: $savedDate'); try { final savedDateTime = DateTime.parse(savedDate); final difference = currentDate.difference(savedDateTime).inSeconds.abs(); if (difference > 5) { print( '⚠️ WARNING: Saved date differs from expected by $difference seconds'); print('⚠️ Expected: $formattedCurrentDate, Got: $savedDate'); } else { print('✅ Date verification successful'); } } catch (e) { print('❌ Date verification error: $e'); } } setState(() { _updateProgress = 1.0; _updateInfo = 'sync_complete'.tr(); }); if (kDebugMode) { print('✅ Sync completed'); print('📊 Total documents processed: $processedItems'); } // Update Live Activity to show the completion if (_syncActivityId != null && Platform.isIOS && _liveActivitiesService.isSupported) { try { // First update with sync complete message await _liveActivitiesService.updateSearchActivity( activityId: _syncActivityId!, title: 'sync_complete'.tr(), query: 'processed_items'.tr(args: ['$processedItems', '$totalItems']), count: processedItems.toString(), ); // Wait a moment for the update to be visible await Future.delayed(const Duration(seconds: 2)); // Then end the activity await _liveActivitiesService.endSearchActivity(_syncActivityId!); _syncActivityId = null; } catch (e) { if (kDebugMode) { print('Error finalizing Live Activity: $e'); } } } await Future.delayed(const Duration(seconds: 1)); await _showSyncNotification( 'processed_items'.tr(args: ['$processedItems', '$totalItems'])); } } // Create a Live Activity with proper delay and verification Future _createLiveActivity( String title, String query, String count) async { if (!Platform.isIOS) return null; try { // Small delay to ensure everything is properly initialized await Future.delayed(const Duration(milliseconds: 500)); // Verify the service is ready if (!_liveActivitiesService.isInitialized || !_liveActivitiesService.isSupported) { if (kDebugMode) { print( 'Live Activities service not ready: initialized=${_liveActivitiesService.isInitialized}, supported=${_liveActivitiesService.isSupported}'); } return null; } // Verify the appGroupId is set if (_liveActivitiesService.appGroupId == null) { if (kDebugMode) { print('AppGroupId is null, reinitializing Live Activities...'); } // Try to reinitialize await _liveActivitiesService.init( appGroupId: 'group.com.carpa.searchEngine.widget', urlScheme: 'lgcc'); // Small delay after reinitialization await Future.delayed(const Duration(milliseconds: 300)); } // Create the activity final activityId = await _liveActivitiesService.createSearchActivity( title: title, query: query, count: count, ); if (kDebugMode) { print('Created Live Activity with ID: $activityId'); } return activityId; } catch (e) { if (kDebugMode) { print('Error creating Live Activity: $e'); } return null; } } // Helper method to update Live Activity safely Future _updateLiveActivity( String? activityId, String title, String query, String count) async { if (activityId == null || !Platform.isIOS) return false; try { // Verify the service is ready if (!_liveActivitiesService.isInitialized || !_liveActivitiesService.isSupported) { if (kDebugMode) { print('Live Activities service not ready for update'); } return false; } // Perform the update final success = await _liveActivitiesService.updateSearchActivity( activityId: activityId, title: title, query: query, count: count, ); if (kDebugMode) { print('Updated Live Activity $activityId: $success'); } return success; } catch (e) { if (kDebugMode) { print('Error updating Live Activity: $e'); } return false; } } // Helper method to end Live Activity safely Future _endLiveActivity(String? activityId) async { if (activityId == null || !Platform.isIOS) return false; try { // Verify the service is ready if (!_liveActivitiesService.isInitialized || !_liveActivitiesService.isSupported) { if (kDebugMode) { print('Live Activities service not ready for ending activity'); } return false; } // End the activity final success = await _liveActivitiesService.endSearchActivity(activityId); if (kDebugMode) { print('Ended Live Activity $activityId: $success'); } // Clear the activity ID if (activityId == _syncActivityId) { _syncActivityId = null; } return success; } catch (e) { if (kDebugMode) { print('Error ending Live Activity: $e'); } return false; } } }