import 'dart:async'; import 'dart:io'; 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_dotenv/flutter_dotenv.dart'; import 'package:path_provider/path_provider.dart'; import 'package:search_engine/database.dart'; import 'package:search_engine/screens/config.dart'; import 'package:search_engine/screens/content.dart'; import 'package:search_engine/screens/generic_search.dart'; import 'package:search_engine/widgets/base.dart'; import 'package:search_engine/widgets/navigation_bar.dart'; import 'package:dio/dio.dart'; import 'package:search_engine/screens/pdf.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:search_engine/utils.dart' as utils; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart' as nav; import 'package:flutter/services.dart'; import 'package:search_engine/services/config_service.dart'; // Extensión para capitalizar strings extension StringExtension on String { String capitalize() { if (isEmpty) return this; return '${this[0].toUpperCase()}${substring(1)}'; } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override // ignore: library_private_types_in_public_api _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { late Directory appDirectory; late Future _databaseFuture; List allMessages = []; // Unified list for all messages Timer? _debounce; final TextEditingController _searchController = TextEditingController(); final _baseUrl = dotenv.env['BASE_URL']; final _token = dotenv.env['TOKEN']; late String locale; List favorites = []; final bool _showSearchOverlay = false; final _searchOverlayController = TextEditingController(); Timer? _searchDebounce; List _searchResults = []; bool _isSearching = false; final _searchFocusNode = FocusNode(); // Estados de carga bool isLoadingMessages = false; bool isLoadingFavorites = true; // Variables para el filtro por año y mes List _availableYears = []; List _availableMonths = []; String? _selectedYear; String? _selectedMonth; bool _isFiltered = false; bool isLoadingYears = false; bool isLoadingMonths = false; // Controlador para la carga progresiva final ScrollController _messagesScrollController = ScrollController(); // Caché de miniaturas final Map _thumbnailCache = {}; String? _error; @override void initState() { super.initState(); _databaseFuture = Future.value(AppDatabase()); _initAppDirectory(); _loadLocale().then((_) { _loadData(); _loadAvailableYears(); }); } @override void dispose() { _messagesScrollController.dispose(); _debounce?.cancel(); _searchDebounce?.cancel(); super.dispose(); } Future _loadData() async { try { setState(() { isLoadingMessages = true; _error = null; }); final token = RootIsolateToken.instance; if (token == null) { throw Exception('RootIsolateToken is not initialized'); } final messagesResult = await compute( _getAllMessagesIsolate, [locale, token, 15], // Limitar a 15 mensajes inicialmente ); if (mounted) { setState(() { allMessages = messagesResult; isLoadingMessages = false; }); } } catch (e) { if (kDebugMode) { print('Error loading messages: $e'); } if (mounted) { setState(() { _error = e.toString(); isLoadingMessages = false; }); } } } Future _loadAllMessages() async { try { setState(() { isLoadingMessages = true; }); final token = RootIsolateToken.instance; if (token == null) { throw Exception('RootIsolateToken is not initialized'); } final messagesResult = await compute( _getAllMessagesIsolate, [locale, token, 15], // Limitar a 15 mensajes inicialmente ); if (mounted) { setState(() { allMessages = messagesResult; isLoadingMessages = false; }); } } catch (e) { if (kDebugMode) { print('Error fetching all messages: $e'); } if (mounted) { setState(() { allMessages = []; isLoadingMessages = false; }); } } } Future _loadLocale() async { final newLocale = await ConfigService.getLocale(); if (mounted) { setState(() { locale = newLocale; }); } } // Método para cargar los años disponibles Future _loadAvailableYears() async { setState(() { isLoadingYears = true; _availableYears = []; }); try { final database = await _databaseFuture; final years = await database.getAvailableYears(); setState(() { _availableYears = years; isLoadingYears = false; }); } catch (e) { setState(() { _availableYears = []; isLoadingYears = false; }); } } // Método para cargar los meses disponibles para un año seleccionado Future _loadAvailableMonths(String year) async { setState(() { isLoadingMonths = true; _availableMonths = []; }); try { final database = await _databaseFuture; final months = await database.getMonths(year); setState(() { _availableMonths = months; isLoadingMonths = false; }); } catch (e) { setState(() { _availableMonths = []; isLoadingMonths = false; }); } } // Funciones estáticas para los isolates static Future> _getAllMessagesIsolate( List params) async { try { final locale = params[0] as String; final token = params[1] as RootIsolateToken; final limit = params.length > 2 ? params[2] as int : null; BackgroundIsolateBinaryMessenger.ensureInitialized(token); final database = AppDatabase(); return database.getAllMessages(locale, limit); } catch (e) { if (kDebugMode) { print('Error in _getAllMessagesIsolate: $e'); } return []; } } static Future> _getFilteredMessagesIsolate( List params) async { try { final year = params[0] as String; final month = params[1] as String; final locale = params[2] as String; final token = params[3] as RootIsolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(token); final database = AppDatabase(); return database.getFilteredMessages(year, month, locale); } catch (e) { if (kDebugMode) { print('Error in _getFilteredMessagesIsolate: $e'); } return []; } } static Future> _searchMessagesIsolate( List params) async { try { final query = params[0] as String; final locale = params[1] as String; final token = params[2] as RootIsolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(token); final database = AppDatabase(); return database.searchMessages(query, locale); } catch (e) { if (kDebugMode) { print('Error in _searchMessagesIsolate: $e'); } return []; } } // Método para aplicar filtros y cargar datos filtrados Future _applyFilters() async { if (_selectedYear != null && _selectedMonth != null) { setState(() { _isFiltered = true; isLoadingMessages = true; }); try { final token = RootIsolateToken.instance; if (token == null) { throw Exception('RootIsolateToken is not initialized'); } final filteredMessages = await compute( _getFilteredMessagesIsolate, [_selectedYear!, _selectedMonth!, locale, token], ); if (mounted) { setState(() { allMessages = filteredMessages; isLoadingMessages = false; }); } } catch (e) { if (mounted) { setState(() { _error = e.toString(); isLoadingMessages = false; }); } } } } // Método para limpiar filtros void _clearFilters() { setState(() { _selectedYear = null; _selectedMonth = null; _isFiltered = false; }); _loadAllMessages(); } void _onSearch(String value) { if (_debounce?.isActive ?? false) _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () async { if (value.isNotEmpty) { nav.pushScreenWithoutNavBar( context, GenericSearchPage( searchTerm: value, title: 'search'.tr(), hintText: 'search_placeholder'.tr(), searchFunction: (query, pageKey, offset) async { final token = RootIsolateToken.instance; if (token == null) { throw Exception('RootIsolateToken is not initialized'); } return await compute( _searchMessagesIsolate, [query, locale, token]); }, ), ); } }); } Future _initAppDirectory() async { appDirectory = await getApplicationDocumentsDirectory(); } // Cargar datos de forma progresiva Future _loadFavorites() async { try { final fav = await _databaseFuture.then((database) => database.getFavorites()); if (mounted) { setState(() { favorites = fav; isLoadingFavorites = false; }); } } catch (e) { if (kDebugMode) { print('Error fetching favorites: $e'); } if (mounted) { setState(() { favorites = []; isLoadingFavorites = false; }); } } } Future _getThumbnail(String directoryPath, String fileId) async { // Verificar si la miniatura ya está en caché final cacheKey = '$directoryPath/$fileId'; if (_thumbnailCache.containsKey(cacheKey)) { return _thumbnailCache[cacheKey]; } if (fileId.isEmpty) { _thumbnailCache[cacheKey] = null; return null; } try { final dir = Directory(directoryPath); if (!await dir.exists()) { await dir.create(recursive: true); } final String thumbnailPath = '$directoryPath/$fileId+SD.jpg'; final thumbnailFile = File(thumbnailPath); // Verificar si el archivo ya existe localmente if (await thumbnailFile.exists()) { _thumbnailCache[cacheKey] = thumbnailFile; return thumbnailFile; } // Descargar la miniatura si no existe final String url = '$_baseUrl/assets/$fileId?access_token=$_token&width=320&height=180&quality=50&fit=cover&format=jpg'; await Dio().download(url, thumbnailPath); final file = File(thumbnailPath); _thumbnailCache[cacheKey] = file; return file; } catch (e) { if (kDebugMode) { print('Error downloading thumbnail: $e'); } _thumbnailCache[cacheKey] = null; return null; } } Widget _buildThumbnail(String thumbnailId) { return FutureBuilder( future: _getThumbnail( '${appDirectory.path}/LGCC_Search/$locale/thumbnails/', thumbnailId), builder: (context, snapshot) { return Skeletonizer( enableSwitchAnimation: true, enabled: snapshot.connectionState != ConnectionState.done, effect: const ShimmerEffect( baseColor: Color(0xFFf1f5eb), highlightColor: Colors.white30, duration: Duration(milliseconds: 1000), ), child: snapshot.hasData && snapshot.data != null ? Image.file( snapshot.data!, height: double.infinity, width: double.infinity, fit: BoxFit.cover, cacheHeight: 180, cacheWidth: 320, ) : Image.asset( 'assets/image/default_thumbnail.jpg', height: double.infinity, width: double.infinity, fit: BoxFit.cover, alignment: Alignment.topCenter, cacheHeight: 180, cacheWidth: 320, ), ); }, ); } Future _performSearch(String query) async { try { final results = await _databaseFuture .then((database) => database.searchMessages(query, locale)); setState(() { _searchResults = results; _isSearching = false; }); } catch (e) { if (kDebugMode) { print('Error performing search: $e'); } setState(() { _searchResults = []; _isSearching = false; }); } } Widget _buildSearchResultsOverlay() { if (!_showSearchOverlay) return const SizedBox.shrink(); return Positioned( top: 80, // Adjust this value based on your header height left: 24, right: 24, child: Material( elevation: 8, borderRadius: BorderRadius.circular(12), child: Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_isSearching) const Padding( padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(), ) else if (_searchResults.isEmpty) Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.search_off, size: 64, color: Color(0XFF6b8e23), ), const SizedBox(height: 16), Text( 'no_results'.tr(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ) else Flexible( child: ListView.builder( shrinkWrap: true, itemCount: _searchResults.length, itemBuilder: (context, index) { final message = _searchResults[index]; return _buildListCard(message); }, ), ), ], ), ), ), ); } @override Widget build(BuildContext context) { return Stack( children: [ BaseScreen( title: 'home'.tr(), showSearchBar: true, showSettingsButton: true, searchController: _searchController, onSearchChanged: _onSearch, searchHintText: 'search_placeholder'.tr(), child: RefreshIndicator.adaptive( onRefresh: _refreshData, child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), if (_error != null) Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.red[100], borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red[700]), const SizedBox(width: 8), Expanded( child: Text( _error!, style: TextStyle(color: Colors.red[700]), ), ), ], ), ), _buildAllMessagesSection(), ], ), ), ); }, ), ), ), _buildSearchResultsOverlay(), ], ); } Widget _buildDrawer(BuildContext context) { return Drawer( child: ListView( padding: EdgeInsets.zero, children: [ DrawerHeader( decoration: const BoxDecoration( color: Color(0xFF6b8e23), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const CircleAvatar( radius: 40, backgroundColor: Colors.white, child: Icon( Icons.menu_book, size: 40, color: Color(0xFF6b8e23), ), ), const SizedBox(height: 10), Text( 'title'.tr(), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ], ), ), ListTile( leading: const Icon(Icons.home), title: Text('title'.tr()), onTap: () { Navigator.pop(context); }, ), ListTile( leading: const Icon(Icons.search), title: Text('search'.tr()), onTap: () { Navigator.pop(context); GlobalNavigator.navigateToIndex(context, 0); }, ), ListTile( leading: const Icon(Icons.calendar_today), title: Text('calendar'.tr()), onTap: () { Navigator.pop(context); GlobalNavigator.navigateToIndex(context, 2); }, ), ListTile( leading: const Icon(Icons.menu_book), title: Text('library'.tr()), onTap: () { Navigator.pop(context); GlobalNavigator.navigateToIndex(context, 2); }, ), const Divider(), ListTile( leading: const Icon(Icons.settings), title: Text('settings'.tr()), onTap: () { Navigator.pop(context); nav.pushScreenWithoutNavBar(context, const ConfigView()); }, ), ], ), ); } Widget _buildAllMessagesSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _isFiltered ? '${DateFormat('MMMM', locale).format(DateTime(0, int.parse(_selectedMonth!))).capitalize()} $_selectedYear' : 'last_activities'.tr(), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700), overflow: TextOverflow.ellipsis, maxLines: 1, ), Padding( padding: const EdgeInsets.only(right: 8.0), child: Row( children: [ if (_isFiltered) TextButton.icon( icon: const Icon(Icons.clear, color: Colors.red), label: Text( 'clear_filters'.tr(), style: const TextStyle(color: Colors.red), ), onPressed: _clearFilters, ), ElevatedButton( onPressed: _showFilterDialog, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6b8e23), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.filter_list, size: 18, color: Colors.white), const SizedBox(width: 4), Text( 'filter'.tr(), style: const TextStyle(fontSize: 13), ), ], ), ), ], ), ), ], ), const SizedBox(height: 16), if (isLoadingMessages) _buildSkeletonLoaders() else if (allMessages.isEmpty) Container( height: 260, alignment: Alignment.center, child: Text(_isFiltered ? 'no_activities_for_period'.tr() : 'no_recent_activities'.tr()), ) else _buildUnifiedMessagesList(), ], ); } Widget _buildSkeletonLoaders() { // Determinar si la pantalla es lo suficientemente grande para mostrar grid final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; final isWideScreen = screenWidth > 600 || screenWidth > screenHeight; // Calcular el número de columnas basado en el ancho de pantalla int crossAxisCount = 2; if (screenWidth > screenHeight) { // En modo horizontal if (screenWidth > 900) { crossAxisCount = 4; } else if (screenWidth > 600) { crossAxisCount = 3; } else { crossAxisCount = 2; } } else { // En modo vertical if (screenWidth > 600) { crossAxisCount = 2; } else { crossAxisCount = 1; } } if (isWideScreen) { // Skeleton loaders para grid return OrientationBuilder(builder: (context, orientation) { return GridView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: 0.75, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: 6, itemBuilder: (context, index) { return _buildGridSkeletonLoader(); }, ); }); } else { // Skeleton loaders para lista return ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, separatorBuilder: (context, index) => const SizedBox(height: 12), itemCount: 6, itemBuilder: (context, index) { return _buildListSkeletonLoader(); }, ); } } Widget _buildGridSkeletonLoader() { return Card( elevation: 3, shadowColor: Colors.black26, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFFecefe2), Color(0xFFdfe7d0), ], begin: Alignment.topRight, end: Alignment.bottomLeft, ), borderRadius: BorderRadius.all(Radius.circular(12)), ), child: Skeletonizer( enabled: true, effect: const ShimmerEffect( baseColor: Color(0xFFdfe7d0), highlightColor: Colors.white70, duration: Duration(milliseconds: 1000), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Thumbnail skeleton ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), child: AspectRatio( aspectRatio: 16 / 9, child: Container( color: Colors.grey[300], ), ), ), // Content skeleton Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 16, width: double.infinity, color: Colors.grey[300], ), const SizedBox(height: 4), Container( height: 14, width: 100, color: Colors.grey[300], ), const SizedBox(height: 4), Container( height: 12, width: 120, color: Colors.grey[300], ), const SizedBox(height: 4), Container( height: 12, width: 80, color: Colors.grey[300], ), ], ), ), ), ], ), ), ), ); } Widget _buildListSkeletonLoader() { return Card( elevation: 3, shadowColor: Colors.black26, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFFecefe2), Color(0xFFdfe7d0), ], begin: Alignment.topRight, end: Alignment.bottomLeft, ), borderRadius: BorderRadius.all(Radius.circular(12)), ), child: Skeletonizer( enabled: true, effect: const ShimmerEffect( baseColor: Color(0xFFdfe7d0), highlightColor: Colors.white70, duration: Duration(milliseconds: 1000), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Thumbnail skeleton - ahora más cuadrado ClipRRect( borderRadius: BorderRadius.circular(10), child: Container( width: 130, height: 130, color: Colors.grey[300], ), ), const SizedBox(width: 16), // Content skeleton - con más espacio vertical Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 20, width: double.infinity, color: Colors.grey[300], ), const SizedBox(height: 8), Container( height: 16, width: 120, color: Colors.grey[300], ), const SizedBox(height: 8), Container( height: 14, width: 150, color: Colors.grey[300], ), const SizedBox(height: 8), Container( height: 14, width: 100, color: Colors.grey[300], ), ], ), ), ], ), ), ), ), ); } Widget _buildUnifiedMessagesList() { // Determinar si la pantalla es lo suficientemente grande para mostrar grid final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; final isWideScreen = screenWidth > 600 || screenWidth > screenHeight; // Calcular el número de columnas basado en el ancho de pantalla int crossAxisCount = 2; if (screenWidth > screenHeight) { // En modo horizontal if (screenWidth > 900) { crossAxisCount = 4; } else if (screenWidth > 600) { crossAxisCount = 3; } else { crossAxisCount = 2; } } else { // En modo vertical if (screenWidth > 600) { crossAxisCount = 2; } else { crossAxisCount = 1; } } if (isWideScreen) { // Mostrar grid en pantallas anchas o en orientación horizontal return OrientationBuilder(builder: (context, orientation) { final isLandscape = orientation == Orientation.landscape; return GridView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: isLandscape ? 1.1 : 0.85, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: allMessages.length, itemBuilder: (context, index) { final message = allMessages[index]; return _buildGridCard(message); }, ); }); } else { // Mostrar lista en pantallas estrechas return ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, separatorBuilder: (context, index) => const SizedBox(height: 12), itemCount: allMessages.length, itemBuilder: (context, index) { final message = allMessages[index]; return _buildListCard(message); }, ); } } Widget _buildGridCard(Draft message) { return Card( elevation: 3, shadowColor: Colors.black26, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFFecefe2), Color(0xFFdfe7d0), ], begin: Alignment.topRight, end: Alignment.bottomLeft, ), borderRadius: BorderRadius.all(Radius.circular(12)), ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: () => nav.pushScreenWithoutNavBar(context, TextViewer(data: message)), borderRadius: BorderRadius.circular(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 16 / 9, child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), child: Stack( fit: StackFit.expand, children: [ SizedBox( width: double.infinity, height: double.infinity, child: _buildThumbnail(message.thumbnail), ), _buildMessageIndicators(message), ], ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message.title.isNotEmpty ? message.title : utils.formatDate(message.date, locale), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( utils.formatDate(message.date, locale), style: const TextStyle( color: Colors.grey, fontSize: 12, height: 1.2, ), ), if (message.city.isNotEmpty || message.country.isNotEmpty) Text( message.city.isNotEmpty ? '${message.city}, ${_getCountryName(message.country)}' : _getCountryName(message.country), style: const TextStyle( color: Colors.grey, fontSize: 12, height: 1.2, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (message.activity > 0) Text( '${plural('activity', 1)} ${message.activity}', style: const TextStyle( fontSize: 12, color: Colors.grey, height: 1.2, ), ), ], ), ), ), ], ), ), ), ), ); } Widget _buildListCard(Draft message) { return Card( elevation: 3, shadowColor: Colors.black26, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFFecefe2), Color(0xFFdfe7d0), ], begin: Alignment.topRight, end: Alignment.bottomLeft, ), borderRadius: BorderRadius.all(Radius.circular(12)), ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: () => nav.pushScreenWithoutNavBar(context, TextViewer(data: message)), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(10), child: SizedBox( width: 130, height: 130, child: Stack( fit: StackFit.expand, children: [ _buildThumbnail(message.thumbnail), _buildMessageIndicators(message, isListView: true), ], ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message.title.isNotEmpty ? message.title : utils.formatDate(message.date, locale), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 20, height: 1.2, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( utils.formatDate(message.date, locale), style: const TextStyle( color: Colors.grey, fontSize: 14, height: 1.2, ), ), if (message.city.isNotEmpty || message.country.isNotEmpty) Text( message.city.isNotEmpty ? '${message.city}, ${_getCountryName(message.country)}' : _getCountryName(message.country), style: const TextStyle( color: Colors.grey, fontSize: 14, height: 1.2, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (message.activity > 0) Text( '${plural('activity', 1)} ${message.activity}', style: const TextStyle( fontSize: 14, color: Colors.grey, height: 1.2, ), ), ], ), ), ], ), ), ), ), ), ); } // Método para construir los indicadores (draft, PDF, favorito) Widget _buildMessageIndicators(Draft message, {bool isListView = false}) { return Stack( fit: StackFit.expand, children: [ // Indicadores de Draft y PDF en la esquina superior izquierda Positioned( top: 4, left: 4, child: Row( children: [ // Draft indicator if (message.draft == 1) Container( decoration: BoxDecoration( color: Colors.red.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), padding: EdgeInsets.symmetric( horizontal: isListView ? 8 : 6, vertical: isListView ? 4 : 2), child: Text( 'draft'.tr(), style: TextStyle( color: Colors.white, fontSize: isListView ? 12 : 12, fontWeight: FontWeight.bold, ), ), ), // Espacio entre indicadores si ambos están presentes if (message.draft == 1 && message.pdf != null && message.pdf!.isNotEmpty) const SizedBox(width: 6), // PDF indicator if (message.pdf != null && message.pdf!.isNotEmpty) InkWell( onTap: () { nav.pushScreenWithoutNavBar( context, FilePdf(pdf: message.pdf!, title: message.title), ); }, child: Container( decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(4), ), padding: EdgeInsets.all(isListView ? 4 : 4), child: Icon( Icons.picture_as_pdf, color: Colors.white, size: isListView ? 18 : 16, ), ), ), ], ), ), // Favorite indicator (se mantiene en la esquina inferior derecha) if (favorites.contains(message.id)) Positioned( bottom: 4, right: 4, child: Container( decoration: BoxDecoration( color: Colors.green.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), padding: EdgeInsets.all(isListView ? 4 : 4), child: Icon( Icons.bookmark, color: Colors.white, size: isListView ? 18 : 16, ), ), ), ], ); } // Método para mostrar el diálogo de filtro void _showFilterDialog() { showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'filter'.tr(), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF6b8e23), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), content: Container( width: double.maxFinite, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.6, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'select_year'.tr(), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), if (isLoadingYears) const Center(child: CircularProgressIndicator()) else SizedBox( height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _availableYears.length, itemBuilder: (context, index) { final year = _availableYears[index]; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(year), selected: _selectedYear == year, onSelected: (selected) { setDialogState(() { _selectedYear = selected ? year : null; _selectedMonth = null; if (selected) { _loadAvailableMonths(year); } else { _availableMonths = []; } }); }, backgroundColor: const Color(0xFFecefe2), selectedColor: const Color(0xFF6b8e23), labelStyle: TextStyle( color: _selectedYear == year ? Colors.white : Colors.black, ), ), ); }, ), ), const SizedBox(height: 24), Text( 'select_month'.tr(), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), if (_selectedYear == null) Center( child: Text( 'select_year_first'.tr(), style: TextStyle(color: Colors.grey), ), ) else if (isLoadingMonths) const Center(child: CircularProgressIndicator()) else if (_availableMonths.isEmpty) Center( child: Text( 'no_months_available'.tr(), style: TextStyle(color: Colors.grey), ), ) else SizedBox( height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _availableMonths.length, itemBuilder: (context, index) { final month = _availableMonths[index]; final monthName = DateFormat('MMMM', locale) .format(DateTime(0, int.parse(month))) .capitalize(); return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(monthName), selected: _selectedMonth == month, onSelected: (selected) { setDialogState(() { _selectedMonth = selected ? month : null; }); }, backgroundColor: const Color(0xFFecefe2), selectedColor: const Color(0xFF6b8e23), labelStyle: TextStyle( color: _selectedMonth == month ? Colors.white : Colors.black, ), ), ); }, ), ), ], ), ), ), actions: [ TextButton( onPressed: () { setDialogState(() { _selectedYear = null; _selectedMonth = null; _availableMonths = []; }); }, child: Text( 'clear'.tr(), style: const TextStyle(color: Colors.grey), ), ), ElevatedButton( onPressed: _selectedYear != null && _selectedMonth != null ? () { Navigator.pop(context); _applyFilters(); } : null, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6b8e23), foregroundColor: Colors.white, ), child: Text('apply'.tr()), ), ], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), backgroundColor: Colors.white, ); }, ), ); } // Método para refrescar todos los datos Future _refreshData() async { setState(() { isLoadingMessages = true; isLoadingFavorites = true; }); if (_isFiltered && _selectedYear != null && _selectedMonth != null) { await _applyFilters(); } else { await Future.wait([ _loadFavorites(), _loadAllMessages(), ]); } // Limpiar caché de miniaturas al refrescar _thumbnailCache.clear(); } String _getCountryName(String countryCode) { if (countryCode.isEmpty) { return 'N/A'; } try { return CountryCodes.detailsFromAlpha2(countryCode).name.toString(); } catch (e) { // Si no se encuentra el código de país, devolver el código como está return countryCode; } } }