1547 lines
50 KiB
Dart
1547 lines
50 KiB
Dart
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<HomePage> {
|
|
late Directory appDirectory;
|
|
late Future<AppDatabase> _databaseFuture;
|
|
List<Draft> 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<String> favorites = [];
|
|
final bool _showSearchOverlay = false;
|
|
final _searchOverlayController = TextEditingController();
|
|
Timer? _searchDebounce;
|
|
List<Draft> _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<String> _availableYears = [];
|
|
List<String> _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<String, File?> _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<void> _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<void> _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<void> _loadLocale() async {
|
|
final newLocale = await ConfigService.getLocale();
|
|
if (mounted) {
|
|
setState(() {
|
|
locale = newLocale;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Método para cargar los años disponibles
|
|
Future<void> _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<void> _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<List<Draft>> _getAllMessagesIsolate(
|
|
List<dynamic> 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<List<Draft>> _getFilteredMessagesIsolate(
|
|
List<dynamic> 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<List<Draft>> _searchMessagesIsolate(
|
|
List<dynamic> 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<void> _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<void> _initAppDirectory() async {
|
|
appDirectory = await getApplicationDocumentsDirectory();
|
|
}
|
|
|
|
// Cargar datos de forma progresiva
|
|
Future<void> _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<File?> _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<File?>(
|
|
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<void> _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<void> _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;
|
|
}
|
|
}
|
|
}
|