md-app/lib/screens/landing.dart

1208 lines
41 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<LandingPage> {
double _updateProgress = 0.0;
late Future<AppDatabase> _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<void> 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<dynamic> data = jsonResponse['data'] ?? [];
if (data.isEmpty) {
hasMoreData = false;
continue;
}
// Procesar datos en un isolate
final processedData = await compute(
_processBatchItemsIsolate,
data,
);
final List<Draft> batchMessages = processedData['messages'];
final List<Map<String, String>> 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<int> _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<Map<String, dynamic>> _processBatchItemsIsolate(
List<dynamic> data) async {
final List<Draft> batchMessages = [];
final List<Map<String, String>> 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<String, dynamic> 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<double>(
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<void> _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<void> _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<Map<String, dynamic>> _checkDatabaseStatsIsolate(
List<dynamic> 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<Map<String, dynamic>> _syncMimirWithDatabaseIsolate(
List<dynamic> 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<String>.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<String> 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<String> 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<String> getLocale() async {
final locale = await ConfigService.getLocale();
initializeDateFormatting(locale);
return locale;
}
// Helper method to get the latest date from server
Future<String?> _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<dynamic> 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<void> _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<void> _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<String?> _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<bool> _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<bool> _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;
}
}
}