1208 lines
41 KiB
Dart
1208 lines
41 KiB
Dart
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;
|
||
}
|
||
}
|
||
}
|