md-app/lib/screens/home.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;
}
}
}