diff --git a/.gitignore b/.gitignore index a6a78f7..e0acfcf 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ test/**/build/ *.pem env_config.txt + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/lib/about.dart b/lib/about.dart index 7b307a1..eb4d5a0 100644 --- a/lib/about.dart +++ b/lib/about.dart @@ -17,7 +17,8 @@ class AboutPage extends StatelessWidget { @override Widget build(BuildContext context) { final appState = Provider.of(context, listen: false); - final textSizeOffset = Provider.of(context, listen: false).getTextSizeOffset(); + final textSizeOffset = + Provider.of(context, listen: false).getTextSizeOffset(); return Scaffold( appBar: AppBar( @@ -27,7 +28,9 @@ class AboutPage extends StatelessWidget { S.of(context).about, style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 17 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 17 + + Provider.of(context, listen: false) + .getTextSizeOffset(), ), ), ), @@ -42,7 +45,7 @@ class AboutPage extends StatelessWidget { S.of(context).application, textSizeOffset, ), - + InfoCardWidgets.buildInfoCard( context, icon: CupertinoIcons.info_circle, @@ -50,7 +53,7 @@ class AboutPage extends StatelessWidget { subtitle: S.of(context).appName, textSizeOffset: textSizeOffset, ), - + FutureBuilder( future: _getAppVersion(), builder: (context, snapshot) { @@ -63,7 +66,7 @@ class AboutPage extends StatelessWidget { ); }, ), - + InfoCardWidgets.buildInfoCard( context, icon: CupertinoIcons.person, @@ -72,7 +75,7 @@ class AboutPage extends StatelessWidget { textSizeOffset: textSizeOffset, linkUrl: 'https://linktr.ee/byackee', ), - + InfoCardWidgets.buildLinkCard( context, title: 'Linktree', @@ -89,7 +92,7 @@ class AboutPage extends StatelessWidget { S.of(context).thanks, textSizeOffset, ), - + InfoCardWidgets.buildThanksCard( context, title: S.of(context).thankYouMessage, diff --git a/lib/app_state.dart b/lib/app_state.dart index 0d91f5f..c0cfb07 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -73,7 +73,8 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { // --- Gestion du compteur d'ouvertures de l'app pour la popup dons --- int _appOpenCount = 0; - int _lastDonationPopupTimestamp = 0; // Timestamp de la dernière fois que la popup a été affichée + int _lastDonationPopupTimestamp = + 0; // Timestamp de la dernière fois que la popup a été affichée int get appOpenCount => _appOpenCount; // Optimisation: Éviter les notifyListeners multiples @@ -82,10 +83,10 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { /// Retourne true si la popup dons doit être affichée (après 10 ouvertures ET au moins 1h depuis la dernière) bool get shouldShowDonationPopup { if (_appOpenCount < 10) return false; - + final currentTimestamp = DateTime.now().millisecondsSinceEpoch; final oneHourInMillis = 60 * 60 * 1000; // 1 heure en millisecondes - + // Vérifier si au moins 1 heure s'est écoulée depuis la dernière popup return (currentTimestamp - _lastDonationPopupTimestamp) >= oneHourInMillis; } @@ -95,12 +96,14 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { loadAppOpenCount(); // Charger le compteur d'ouvertures loadLastDonationPopupTimestamp(); // Charger le timestamp de la dernière popup incrementAppOpenCount(); // Incrémenter à chaque lancement - WidgetsBinding.instance.addObserver(this); // Add observer to listen to system changes + WidgetsBinding.instance + .addObserver(this); // Add observer to listen to system changes } @override void dispose() { - WidgetsBinding.instance.removeObserver(this); // Remove observer when AppState is disposed + WidgetsBinding.instance + .removeObserver(this); // Remove observer when AppState is disposed super.dispose(); } @@ -117,7 +120,7 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { // Load settings from SharedPreferences Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); - + // Regrouper toutes les mises à jour pour éviter les notifications multiples batchUpdate(() { isDarkTheme = prefs.getBool('isDarkTheme') ?? false; @@ -125,18 +128,20 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { selectedTextSize = prefs.getString('textSize') ?? 'normal'; selectedLanguage = prefs.getString('language') ?? 'en'; evmAddresses = prefs.getStringList('evmAddresses'); // Load EVM addresses - _showAmounts = prefs.getBool('showAmounts') ?? true; // Charge la préférence du montant affiché + _showAmounts = prefs.getBool('showAmounts') ?? + true; // Charge la préférence du montant affiché // Charger les paramètres du portfolio _showTotalInvested = prefs.getBool('showTotalInvested') ?? false; _showNetTotal = prefs.getBool('showNetTotal') ?? true; _manualAdjustment = prefs.getDouble('manualAdjustment') ?? 0.0; _showYamProjection = prefs.getBool('showYamProjection') ?? true; - _initialInvestmentAdjustment = prefs.getDouble('initialInvestmentAdjustment') ?? 0.0; - + _initialInvestmentAdjustment = + prefs.getDouble('initialInvestmentAdjustment') ?? 0.0; + _applyTheme(); // Apply the theme based on the loaded themeMode }); - + // Charger la couleur primaire séparément car elle est asynchrone _primaryColor = await getSavedPrimaryColor(); // Load primary color _notifyListenersIfNeeded(); @@ -151,7 +156,8 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { void toggleShowAmounts() async { _showAmounts = !_showAmounts; final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('showAmounts', _showAmounts); // Sauvegarde la préférence utilisateur + await prefs.setBool( + 'showAmounts', _showAmounts); // Sauvegarde la préférence utilisateur _notifyListenersIfNeeded(); // Notifie les widgets dépendants } @@ -190,7 +196,8 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { // Update dark/light theme directly and save to SharedPreferences (for manual switch) void updateTheme(bool value) async { isDarkTheme = value; - themeMode = value ? 'dark' : 'light'; // Set theme mode based on manual selection + themeMode = + value ? 'dark' : 'light'; // Set theme mode based on manual selection final prefs = await SharedPreferences.getInstance(); await prefs.setBool('isDarkTheme', value); await prefs.setString('themeMode', themeMode); // Save theme mode @@ -292,13 +299,15 @@ class AppState extends ChangeNotifier with WidgetsBindingObserver { Future loadLastDonationPopupTimestamp() async { final prefs = await SharedPreferences.getInstance(); - _lastDonationPopupTimestamp = prefs.getInt('lastDonationPopupTimestamp') ?? 0; + _lastDonationPopupTimestamp = + prefs.getInt('lastDonationPopupTimestamp') ?? 0; } Future updateLastDonationPopupTimestamp() async { final prefs = await SharedPreferences.getInstance(); _lastDonationPopupTimestamp = DateTime.now().millisecondsSinceEpoch; - await prefs.setInt('lastDonationPopupTimestamp', _lastDonationPopupTimestamp); + await prefs.setInt( + 'lastDonationPopupTimestamp', _lastDonationPopupTimestamp); } // Méthode pour regrouper les mises à jour diff --git a/lib/components/charts/chart_builders.dart b/lib/components/charts/chart_builders.dart index 9e94951..14a023b 100644 --- a/lib/components/charts/chart_builders.dart +++ b/lib/components/charts/chart_builders.dart @@ -5,13 +5,11 @@ import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/date_utils.dart'; -import 'package:realtoken_asset_tracker/models/apy_record.dart'; import 'package:provider/provider.dart'; /// Factory pour construire les éléments de graphiques de manière standardisée /// Réduit la duplication dans GenericChartWidget class ChartBuilders { - /// Construit les données de grille standardisées pour les graphiques static FlGridData buildStandardGridData() { return FlGridData( @@ -19,7 +17,7 @@ class ChartBuilders { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, @@ -78,7 +76,8 @@ class ChartBuilders { currencyUtils.currencySymbol, ); } else { - formattedValue = '$valuePrefix${value.toStringAsFixed(1)}$valueSuffix'; + formattedValue = + '$valuePrefix${value.toStringAsFixed(1)}$valueSuffix'; } return Padding( @@ -148,7 +147,8 @@ class ChartBuilders { currencyUtils.currencySymbol, ); } else { - formattedValue = '$valuePrefix${value.toStringAsFixed(1)}$valueSuffix'; + formattedValue = + '$valuePrefix${value.toStringAsFixed(1)}$valueSuffix'; } return Padding( @@ -191,7 +191,8 @@ class ChartBuilders { TextStyle( color: Colors.white, fontWeight: FontWeight.w600, - fontSize: 12 + (Provider.of(context, listen: false).getTextSizeOffset()), + fontSize: 12 + + (Provider.of(context, listen: false).getTextSizeOffset()), ), ); } @@ -239,7 +240,7 @@ class ChartBuilders { backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxY, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ); } @@ -275,28 +276,30 @@ class ChartBuilders { final isLast = spot.x == barData.spots.length - 1; final int dataLength = barData.spots.length; int step = 1; - + if (dataLength > 20) { step = (dataLength / 20).ceil(); } else if (dataLength > 10) { step = 2; } - + final isInteresting = spot.x % step == 0; return isFirst || isLast || isInteresting; }, ), - belowBarData: showArea ? BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - color.withOpacity(0.3), - color.withOpacity(0.05), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ) : BarAreaData(show: false), + belowBarData: showArea + ? BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + color.withValues(alpha: 0.3), + color.withValues(alpha: 0.05), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ) + : BarAreaData(show: false), ); } @@ -325,7 +328,8 @@ class ChartBuilders { } /// Construit une clé de période standardisée pour le regroupement de données - static String buildPeriodKey(DateTime date, String selectedPeriod, BuildContext context) { + static String buildPeriodKey( + DateTime date, String selectedPeriod, BuildContext context) { if (selectedPeriod == S.of(context).day) { return DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { @@ -346,7 +350,8 @@ class ChartBuilders { } /// Construit un widget de carte vide standardisé - static Widget buildEmptyCard(BuildContext context, AppState appState, String message) { + static Widget buildEmptyCard( + BuildContext context, AppState appState, String message) { return Card( elevation: 0, shape: RoundedRectangleBorder( @@ -367,4 +372,4 @@ class ChartBuilders { ), ); } -} \ No newline at end of file +} diff --git a/lib/components/charts/distribution_chart_builders.dart b/lib/components/charts/distribution_chart_builders.dart index b8dc941..f61c554 100644 --- a/lib/components/charts/distribution_chart_builders.dart +++ b/lib/components/charts/distribution_chart_builders.dart @@ -9,7 +9,6 @@ import 'package:realtoken_asset_tracker/utils/location_utils.dart'; /// Factory pour construire les graphiques de distribution standardisés /// Factorisation des patterns répétitifs dans les graphiques en secteurs class DistributionChartBuilders { - /// Construit les données pour un graphique de distribution par ville static List buildCityDistributionData({ required BuildContext context, @@ -22,7 +21,8 @@ class DistributionChartBuilders { // Remplir le dictionnaire avec les counts par ville for (var token in portfolio) { - String city = token['city'] ?? LocationUtils.extractCity(token['fullName'] ?? ''); + String city = + token['city'] ?? LocationUtils.extractCity(token['fullName'] ?? ''); cityCount[city] = (cityCount[city] ?? 0) + 1; } @@ -47,7 +47,8 @@ class DistributionChartBuilders { // Remplir le dictionnaire avec les counts par région for (var token in portfolio) { - String regionCode = token['regionCode'] ?? LocationUtils.extractRegion(token['fullName'] ?? ''); + String regionCode = token['regionCode'] ?? + LocationUtils.extractRegion(token['fullName'] ?? ''); String regionName = Parameters.getRegionDisplayName(regionCode); regionCount[regionName] = (regionCount[regionName] ?? 0) + 1; } @@ -72,7 +73,8 @@ class DistributionChartBuilders { // Remplir le dictionnaire avec les counts par pays for (var token in portfolio) { - String country = token['country'] ?? LocationUtils.extractCountry(token['fullName'] ?? ''); + String country = token['country'] ?? + LocationUtils.extractCountry(token['fullName'] ?? ''); countryCount[country] = (countryCount[country] ?? 0) + 1; } @@ -98,7 +100,8 @@ class DistributionChartBuilders { for (var token in portfolio) { List wallets = List.from(token['wallets'] ?? []); for (String wallet in wallets) { - String shortWallet = '${wallet.substring(0, 6)}...${wallet.substring(wallet.length - 4)}'; + String shortWallet = + '${wallet.substring(0, 6)}...${wallet.substring(wallet.length - 4)}'; walletCount[shortWallet] = (walletCount[shortWallet] ?? 0) + 1; } } @@ -121,23 +124,24 @@ class DistributionChartBuilders { required String othersKey, }) { final appState = Provider.of(context); - + // Calculer le total int totalCount = dataCount.values.fold(0, (sum, value) => sum + value); - + if (totalCount == 0) { return [ PieChartSectionData( value: 1, title: '', - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), radius: 40, ) ]; } // Trier par count décroissant - final sortedEntries = dataCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = dataCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List sections = []; othersDetails.clear(); @@ -161,15 +165,17 @@ class DistributionChartBuilders { sections.add(PieChartSectionData( value: value.toDouble(), title: '${percentage.toStringAsFixed(1)}%', - color: baseColor.withOpacity(opacity), + color: baseColor.withValues(alpha: opacity), radius: isSelected ? 52 : 45, titleStyle: TextStyle( - fontSize: isSelected ? 14 + appState.getTextSizeOffset() : 10 + appState.getTextSizeOffset(), + fontSize: isSelected + ? 14 + appState.getTextSizeOffset() + : 10 + appState.getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -188,16 +194,21 @@ class DistributionChartBuilders { final bool isOthersSelected = selectedIndex == indexCounter; sections.add(PieChartSectionData( value: othersValue.toDouble(), - title: '${S.of(context).others} ${othersPercentage.toStringAsFixed(1)}%', - color: Colors.grey.shade400.withOpacity(isOthersSelected ? 1.0 : (selectedIndex != null ? 0.5 : 1.0)), + title: + '${S.of(context).others} ${othersPercentage.toStringAsFixed(1)}%', + color: Colors.grey.shade400.withValues( + alpha: + isOthersSelected ? 1.0 : (selectedIndex != null ? 0.5 : 1.0)), radius: isOthersSelected ? 52 : 45, titleStyle: TextStyle( - fontSize: isOthersSelected ? 14 + appState.getTextSizeOffset() : 10 + appState.getTextSizeOffset(), + fontSize: isOthersSelected + ? 14 + appState.getTextSizeOffset() + : 10 + appState.getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -249,7 +260,8 @@ class DistributionChartBuilders { } // Trier par count décroissant pour correspondre aux sections - final sortedEntries = dataCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = dataCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); // Filtrer les entrées dont le pourcentage est < 2% List> visibleEntries = []; @@ -304,7 +316,9 @@ class DistributionChartBuilders { ), const SizedBox(height: 4), Text( - othersDetails.fold(0, (sum, item) => sum + (item['count'] as int)).toString(), + othersDetails + .fold(0, (sum, item) => sum + (item['count'] as int)) + .toString(), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -325,12 +339,13 @@ class DistributionChartBuilders { required String othersKey, }) { final appState = Provider.of(context); - + // Calculer le total int totalCount = dataCount.values.fold(0, (sum, value) => sum + value); // Trier par count décroissant - final sortedEntries = dataCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = dataCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List legendItems = []; int othersValue = 0; @@ -354,16 +369,24 @@ class DistributionChartBuilders { builder: (context, selectedIndex, child) { return InkWell( onTap: () { - selectedIndexNotifier.value = (selectedIndexNotifier.value == indexCounter) ? null : indexCounter; + selectedIndexNotifier.value = + (selectedIndexNotifier.value == indexCounter) + ? null + : indexCounter; }, borderRadius: BorderRadius.circular(8), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), + padding: const EdgeInsets.symmetric( + horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: selectedIndexNotifier.value == indexCounter ? color.withOpacity(0.1) : Colors.transparent, + color: selectedIndexNotifier.value == indexCounter + ? color.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: selectedIndexNotifier.value == indexCounter ? color : Colors.transparent, + color: selectedIndexNotifier.value == indexCounter + ? color + : Colors.transparent, width: 1, ), ), @@ -378,7 +401,7 @@ class DistributionChartBuilders { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ) @@ -390,8 +413,13 @@ class DistributionChartBuilders { key, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: selectedIndexNotifier.value == indexCounter ? color : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: selectedIndexNotifier.value == indexCounter ? FontWeight.w600 : FontWeight.normal, + color: selectedIndexNotifier.value == indexCounter + ? color + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: + selectedIndexNotifier.value == indexCounter + ? FontWeight.w600 + : FontWeight.normal, ), ), ], @@ -414,16 +442,24 @@ class DistributionChartBuilders { builder: (context, selectedIndex, child) { return InkWell( onTap: () { - selectedIndexNotifier.value = (selectedIndexNotifier.value == indexOthers) ? null : indexOthers; + selectedIndexNotifier.value = + (selectedIndexNotifier.value == indexOthers) + ? null + : indexOthers; }, borderRadius: BorderRadius.circular(8), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), + padding: + const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: selectedIndexNotifier.value == indexOthers ? Colors.grey.withOpacity(0.1) : Colors.transparent, + color: selectedIndexNotifier.value == indexOthers + ? Colors.grey.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: selectedIndexNotifier.value == indexOthers ? Colors.grey : Colors.transparent, + color: selectedIndexNotifier.value == indexOthers + ? Colors.grey + : Colors.transparent, width: 1, ), ), @@ -438,7 +474,7 @@ class DistributionChartBuilders { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ) @@ -450,8 +486,12 @@ class DistributionChartBuilders { S.of(context).others, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: selectedIndexNotifier.value == indexOthers ? Colors.grey.shade700 : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: selectedIndexNotifier.value == indexOthers ? FontWeight.w600 : FontWeight.normal, + color: selectedIndexNotifier.value == indexOthers + ? Colors.grey.shade700 + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: selectedIndexNotifier.value == indexOthers + ? FontWeight.w600 + : FontWeight.normal, ), ), ], @@ -485,7 +525,7 @@ class DistributionChartBuilders { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: const Offset(0, 1), ), @@ -499,6 +539,7 @@ class DistributionChartBuilders { final hue = ((index * 57) + 193 * (index % 3)) % 360; final saturation = (0.7 + (index % 5) * 0.06).clamp(0.4, 0.7); final brightness = (0.8 + (index % 3) * 0.2).clamp(0.6, 0.9); - return HSVColor.fromAHSV(1.0, hue.toDouble(), saturation, brightness).toColor(); + return HSVColor.fromAHSV(1.0, hue.toDouble(), saturation, brightness) + .toColor(); } -} \ No newline at end of file +} diff --git a/lib/components/charts/generic_chart_widget.dart b/lib/components/charts/generic_chart_widget.dart index ab3ca29..24b6971 100644 --- a/lib/components/charts/generic_chart_widget.dart +++ b/lib/components/charts/generic_chart_widget.dart @@ -34,13 +34,17 @@ class GenericChartWidget extends StatelessWidget { final String valuePrefix; final String valueSuffix; final double? maxY; // Valeur maximale de l'axe Y (optionnelle) - final bool isStacked; // Nouvel attribut pour indiquer si le graphique doit être empilé - + final bool + isStacked; // Nouvel attribut pour indiquer si le graphique doit être empilé + // Nouveaux paramètres pour les graphiques empilés - final List? stackColors; // Liste des couleurs pour chaque série empilée - final List Function(T)? getStackValues; // Fonction pour obtenir les valeurs des séries empilées - final List? stackLabels; // Étiquettes pour la légende des séries empilées - + final List? + stackColors; // Liste des couleurs pour chaque série empilée + final List Function(T)? + getStackValues; // Fonction pour obtenir les valeurs des séries empilées + final List? + stackLabels; // Étiquettes pour la légende des séries empilées + // Nouveaux paramètres pour le switch cumulatif (utilisé pour les graphiques de loyer) final bool? isCumulative; final Function(bool)? onCumulativeChanged; @@ -95,7 +99,8 @@ class GenericChartWidget extends StatelessWidget { // Vérifier si les données sont valides if (dataList.isEmpty) { - return ChartBuilders.buildEmptyCard(context, appState, S.of(context).noDataAvailable); + return ChartBuilders.buildEmptyCard( + context, appState, S.of(context).noDataAvailable); } return Card( @@ -109,7 +114,7 @@ class GenericChartWidget extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -119,11 +124,12 @@ class GenericChartWidget extends StatelessWidget { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), - padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 12.0, bottom: 16.0), + padding: const EdgeInsets.only( + left: 16.0, right: 16.0, top: 12.0, bottom: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -172,7 +178,8 @@ class GenericChartWidget extends StatelessWidget { barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { - final periodKey = _buildDateLabels(context)[group.x.toInt()]; + final periodKey = + _buildDateLabels(context)[group.x.toInt()]; return ChartBuilders.buildBarTooltip( context: context, periodKey: periodKey, @@ -198,17 +205,22 @@ class GenericChartWidget extends StatelessWidget { valueSuffix: valueSuffix, ), borderData: FlBorderData(show: false), - lineBarsData: isStacked && getStackValues != null && stackColors != null - ? _buildStackedLineChartData(context) - : [ChartBuilders.buildStandardLine( - spots: _buildChartData(context), - color: chartColor, - )], + lineBarsData: isStacked && + getStackValues != null && + stackColors != null + ? _buildStackedLineChartData(context) + : [ + ChartBuilders.buildStandardLine( + spots: _buildChartData(context), + color: chartColor, + ) + ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { - final periodLabel = _buildDateLabels(context)[spot.x.toInt()]; + final periodLabel = + _buildDateLabels(context)[spot.x.toInt()]; return ChartBuilders.buildLineTooltip( periodLabel: periodLabel, value: spot.y, @@ -246,15 +258,15 @@ class GenericChartWidget extends StatelessWidget { if (isStacked && getStackValues != null && stackColors != null) { return _buildCustomStackedBarChartData(context); } - + // Cas spécial pour les APY empilés if (isStacked && dataList.isNotEmpty && dataList.first is APYRecord) { return _buildStackedApyBarChartData(context); } - + // Cas normal pour les barres simples List chartData = _buildChartData(context); - + return chartData .asMap() .entries @@ -270,7 +282,7 @@ class GenericChartWidget extends StatelessWidget { backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxY ?? _calculateMaxY(context), - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -280,21 +292,23 @@ class GenericChartWidget extends StatelessWidget { } // Nouvelle méthode pour construire des barres empilées personnalisées - List _buildCustomStackedBarChartData(BuildContext context) { + List _buildCustomStackedBarChartData( + BuildContext context) { // On filtre d'abord les données selon la plage de temps final filteredData = _filterDataByTimeRange(context, dataList); - + if (filteredData.isEmpty || getStackValues == null || stackColors == null) { return []; } - + // Vérifier que nous avons suffisamment de couleurs final stackCount = getStackValues!(filteredData.first).length; if (stackCount != stackColors!.length) { - debugPrint("❌ Erreur: Le nombre de valeurs empilées ($stackCount) ne correspond pas au nombre de couleurs (${stackColors!.length})"); + debugPrint( + "❌ Erreur: Le nombre de valeurs empilées ($stackCount) ne correspond pas au nombre de couleurs (${stackColors!.length})"); return []; } - + // Regrouper les données selon la période sélectionnée pour limiter le nombre de barres Map> groupedData = {}; for (var record in filteredData) { @@ -304,7 +318,8 @@ class GenericChartWidget extends StatelessWidget { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -316,35 +331,37 @@ class GenericChartWidget extends StatelessWidget { // Trier les périodes List sortedKeys = groupedData.keys.toList()..sort(); - + // Appliquer le step pour limiter le nombre de barres si nécessaire List displayedKeys = ChartUtils.applyStepToLabels(sortedKeys); - + final List barGroups = []; - + // Pour chaque période à afficher for (int i = 0; i < displayedKeys.length; i++) { String periodKey = displayedKeys[i]; - + // Si c'est une étiquette vide (appliquée par le step), sauter cette entrée if (periodKey.isEmpty) continue; - + List? periodRecords = groupedData[periodKey]; if (periodRecords == null || periodRecords.isEmpty) continue; - + // Calculer les valeurs moyennes pour cette période List stackAverages = List.filled(stackCount, 0.0); - + try { for (var record in periodRecords) { final stackValues = getStackValues!(record); for (int j = 0; j < stackCount; j++) { - if (j < stackValues.length && !stackValues[j].isNaN && !stackValues[j].isInfinite) { + if (j < stackValues.length && + !stackValues[j].isNaN && + !stackValues[j].isInfinite) { stackAverages[j] += stackValues[j]; } } } - + // Diviser par le nombre d'enregistrements pour obtenir la moyenne for (int j = 0; j < stackCount; j++) { stackAverages[j] /= periodRecords.length; @@ -353,22 +370,25 @@ class GenericChartWidget extends StatelessWidget { stackAverages[j] = 1.0; } } - + // Créer les éléments empilés double cumulativeValue = 0; List stackItems = []; - + for (int j = 0; j < stackCount; j++) { double startValue = cumulativeValue; cumulativeValue += stackAverages[j]; - + // Vérifier les valeurs invalides if (startValue.isNaN || startValue.isInfinite) startValue = j * 1.0; - if (cumulativeValue.isNaN || cumulativeValue.isInfinite) cumulativeValue = (j + 1) * 1.0; - - stackItems.add(BarChartRodStackItem(startValue, cumulativeValue, stackColors![j])); + if (cumulativeValue.isNaN || cumulativeValue.isInfinite) { + cumulativeValue = (j + 1) * 1.0; + } + + stackItems.add(BarChartRodStackItem( + startValue, cumulativeValue, stackColors![j])); } - + barGroups.add( BarChartGroupData( x: i, @@ -381,7 +401,7 @@ class GenericChartWidget extends StatelessWidget { backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxY ?? _calculateMaxY(context) * 1.1, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -391,7 +411,7 @@ class GenericChartWidget extends StatelessWidget { debugPrint("❌ Erreur lors de la création des barres empilées: $e"); } } - + return barGroups; } @@ -399,9 +419,9 @@ class GenericChartWidget extends StatelessWidget { List _buildStackedApyBarChartData(BuildContext context) { // On filtre d'abord les données selon la plage de temps final filteredData = _filterDataByTimeRange(context, dataList); - + Map> groupedData = {}; - + for (var record in filteredData) { if (record is APYRecord) { DateTime date = record.timestamp; @@ -410,7 +430,8 @@ class GenericChartWidget extends StatelessWidget { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -420,51 +441,54 @@ class GenericChartWidget extends StatelessWidget { groupedData.putIfAbsent(periodKey, () => []).add(record); } } - + List barGroups = []; List sortedKeys = groupedData.keys.toList()..sort(); - + // Appliquer le step pour limiter le nombre de barres si nécessaire - List displayedKeys = isBarChart ? ChartUtils.applyStepToLabels(sortedKeys) : sortedKeys; - + List displayedKeys = + isBarChart ? ChartUtils.applyStepToLabels(sortedKeys) : sortedKeys; + // Définir les couleurs pour les valeurs nettes et brutes final Color netColor = chartColor; final Color grossColor = Color(0xFFFF9500); // Orange pour la partie brute - + for (int i = 0; i < displayedKeys.length; i++) { String periodKey = displayedKeys[i]; - + // Si c'est une étiquette vide (appliquée par le step), sauter cette entrée if (periodKey.isEmpty) continue; - + List? records = groupedData[periodKey]; - if (records == null) continue; // Cette clé peut ne pas exister dans groupedData après le step - + if (records == null) { + continue; // Cette clé peut ne pas exister dans groupedData après le step + } + // Calculer les moyennes des valeurs net et gross pour cette période double netApyAvg = 0; double grossApyAvg = 0; - + if (records.isNotEmpty) { for (var record in records) { // Assurer que nous avons les deux valeurs double netValue = record.netApy ?? record.apy; double grossValue = record.grossApy ?? record.apy; - + netApyAvg += netValue; grossApyAvg += grossValue; } - + netApyAvg /= records.length; grossApyAvg /= records.length; } - + // Calculer la différence entre gross et net pour empiler double netValue = netApyAvg; double grossDiff = grossApyAvg - netApyAvg; - + // Si la différence est négative (cas rare), on affiche juste la valeur nette if (grossDiff < 0) grossDiff = 0; - + barGroups.add( BarChartGroupData( x: i, @@ -474,30 +498,31 @@ class GenericChartWidget extends StatelessWidget { width: 12, borderRadius: const BorderRadius.all(Radius.circular(6)), rodStackItems: [ - BarChartRodStackItem(0, netValue, netColor), // Partie nette (fond) - couleur principale - BarChartRodStackItem(netValue, netValue + grossDiff, grossColor), // Différence brute (haut) - couleur différente + BarChartRodStackItem(0, netValue, + netColor), // Partie nette (fond) - couleur principale + BarChartRodStackItem(netValue, netValue + grossDiff, + grossColor), // Différence brute (haut) - couleur différente ], backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxY ?? _calculateMaxY(context) * 1.2, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], ), ); } - + return barGroups; } List _buildChartData(BuildContext context) { // On filtre d'abord les données selon la plage de temps final filteredData = _filterDataByTimeRange(context, dataList); - - + // Fonction personnalisée pour obtenir la valeur Y - double Function(T) getYValueAdapter = (T record) { + getYValueAdapter(T record) { double value; try { // Cas spécial pour APY records - utiliser netApy si disponible @@ -517,30 +542,34 @@ class GenericChartWidget extends StatelessWidget { debugPrint("❌ Erreur lors de l'obtention de la valeur Y: $e"); return 1.0; } - }; - + } + // Puis on utilise la méthode buildHistoryChartData avec applyStep selon le type de graphique try { List spots = ChartUtils.buildHistoryChartData( context, - filteredData, + filteredData, selectedPeriod, getYValueAdapter, getTimestamp, - applyStep: isBarChart, // N'appliquer le step que pour les graphiques à barres + applyStep: + isBarChart, // N'appliquer le step que pour les graphiques à barres aggregate: title.toLowerCase().contains('loyer') ? "sum" : "average", ); - + // Filtrer les spots pour éliminer tout point avec NaN ou Infinity - spots = spots.where((spot) => - !spot.x.isNaN && - !spot.x.isInfinite && - !spot.y.isNaN && - !spot.y.isInfinite - ).toList(); - + spots = spots + .where((spot) => + !spot.x.isNaN && + !spot.x.isInfinite && + !spot.y.isNaN && + !spot.y.isInfinite) + .toList(); + // Vérifier que tous les spots ont y >= 1.0 - return spots.map((spot) => FlSpot(spot.x, spot.y < 1.0 ? 1.0 : spot.y)).toList(); + return spots + .map((spot) => FlSpot(spot.x, spot.y < 1.0 ? 1.0 : spot.y)) + .toList(); } catch (e) { debugPrint("❌ Erreur lors de la création des données du graphique: $e"); return []; @@ -559,7 +588,8 @@ class GenericChartWidget extends StatelessWidget { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -570,24 +600,29 @@ class GenericChartWidget extends StatelessWidget { } List sortedKeys = groupedData.keys.toList()..sort(); - + // Appliquer le step pour limiter le nombre d'étiquettes seulement pour les graphiques à barres return isBarChart ? ChartUtils.applyStepToLabels(sortedKeys) : sortedKeys; } Widget _buildTimeNavigator(BuildContext context) { final appState = Provider.of(context); - + // Déterminer la plage actuelle à afficher String currentRange = ""; - + if (selectedPeriod == S.of(context).day) { // Pour l'affichage par jour final filteredData = _filterDataByTimeRange(context, dataList); if (filteredData.isNotEmpty) { - final firstDate = filteredData.map((e) => getTimestamp(e)).reduce((a, b) => a.isBefore(b) ? a : b); - final lastDate = filteredData.map((e) => getTimestamp(e)).reduce((a, b) => a.isAfter(b) ? a : b); - currentRange = "${DateFormat('dd/MM/yyyy').format(firstDate)} - ${DateFormat('dd/MM/yyyy').format(lastDate)}"; + final firstDate = filteredData + .map((e) => getTimestamp(e)) + .reduce((a, b) => a.isBefore(b) ? a : b); + final lastDate = filteredData + .map((e) => getTimestamp(e)) + .reduce((a, b) => a.isAfter(b) ? a : b); + currentRange = + "${DateFormat('dd/MM/yyyy').format(firstDate)} - ${DateFormat('dd/MM/yyyy').format(lastDate)}"; } } else if (selectedPeriod == S.of(context).week) { // Pour l'affichage par semaine @@ -608,7 +643,7 @@ class GenericChartWidget extends StatelessWidget { currentRange = "${years.first} - ${years.last}"; } } - + // Si nous n'avons pas pu déterminer la plage, utiliser la plage de temps sélectionnée if (currentRange.isEmpty) { switch (selectedTimeRange) { @@ -625,10 +660,10 @@ class GenericChartWidget extends StatelessWidget { currentRange = "Toutes les données"; } } - + // Déterminer l'offset maximum selon la plage temporelle int maxOffset = 10; // Valeur par défaut - + switch (selectedTimeRange) { case '3months': maxOffset = 8; // Permet de remonter jusqu'à 2 ans @@ -642,16 +677,16 @@ class GenericChartWidget extends StatelessWidget { default: maxOffset = 0; // Pas de navigation pour 'all' } - + // Vérifier si les boutons doivent être activés bool canGoBack = selectedTimeRange != 'all' && timeOffset < maxOffset; bool canGoForward = timeOffset > 0; - + // Ajouter l'offset au texte si offset > 0 if (timeOffset > 0) { currentRange += " (offset: -${timeOffset * 3} mois)"; } - + return Container( padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 0), margin: EdgeInsets.zero, @@ -695,15 +730,15 @@ class GenericChartWidget extends StatelessWidget { ), ); } - + // Méthode pour naviguer dans le temps void _navigateTime(BuildContext context, int direction) { // -1 pour reculer (augmenter l'offset), +1 pour avancer (diminuer l'offset) int newOffset = timeOffset; - + // Déterminer l'offset maximum selon la plage temporelle int maxOffset = 10; // Valeur par défaut - + switch (selectedTimeRange) { case '3months': maxOffset = 8; // Permet de remonter jusqu'à 2 ans @@ -717,7 +752,7 @@ class GenericChartWidget extends StatelessWidget { default: maxOffset = 0; // Pas de navigation pour 'all' } - + if (direction < 0) { // Reculer dans le temps (augmenter l'offset) avec une limite maximale if (timeOffset < maxOffset) { @@ -727,12 +762,12 @@ class GenericChartWidget extends StatelessWidget { // Avancer dans le temps (diminuer l'offset) newOffset = max(0, timeOffset - 1); } - + // Si nous n'avons pas de changement ou si nous sommes déjà à l'offset minimum/maximum, ne rien faire if (newOffset == timeOffset) { return; } - + // Appliquer le nouvel offset et réinitialiser la vue onTimeOffsetChanged(newOffset); } @@ -742,30 +777,33 @@ class GenericChartWidget extends StatelessWidget { if (selectedTimeRange == 'all' || records.isEmpty) { return records; } - + // Date actuelle avec prise en compte du décalage temporel DateTime now = DateTime.now(); DateTime referenceDate; - + // Appliquer le décalage temporel en fonction de la période sélectionnée if (selectedPeriod == S.of(context).day) { // Décalage en jours - referenceDate = now.subtract(Duration(days: timeOffset * 30)); // 30 jours par offset + referenceDate = + now.subtract(Duration(days: timeOffset * 30)); // 30 jours par offset } else if (selectedPeriod == S.of(context).week) { // Décalage en semaines - referenceDate = now.subtract(Duration(days: timeOffset * 7 * 4)); // 4 semaines par offset + referenceDate = now.subtract( + Duration(days: timeOffset * 7 * 4)); // 4 semaines par offset } else if (selectedPeriod == S.of(context).month) { // Décalage en mois - referenceDate = DateTime(now.year, now.month - timeOffset * 3, now.day); // 3 mois par offset + referenceDate = DateTime( + now.year, now.month - timeOffset * 3, now.day); // 3 mois par offset } else if (selectedPeriod == S.of(context).year) { // Décalage en années referenceDate = DateTime(now.year - timeOffset, now.month, now.day); } else { referenceDate = now; } - + DateTime cutoffDate; - + // Déterminer la date limite selon la plage sélectionnée switch (selectedTimeRange) { case '3months': @@ -780,32 +818,33 @@ class GenericChartWidget extends StatelessWidget { default: return records; } - + // Définir la date maximale (la fin de la période) DateTime maxDate = referenceDate.add(const Duration(days: 1)); - + // Filtre des données selon la date calculée et la date max return records.where((record) { DateTime timestamp = getTimestamp(record); - return (timestamp.isAfter(cutoffDate) || - (timestamp.year == cutoffDate.year && - timestamp.month == cutoffDate.month && - timestamp.day >= cutoffDate.day)) && - timestamp.isBefore(maxDate); + return (timestamp.isAfter(cutoffDate) || + (timestamp.year == cutoffDate.year && + timestamp.month == cutoffDate.month && + timestamp.day >= cutoffDate.day)) && + timestamp.isBefore(maxDate); }).toList(); } // Méthode utilitaire pour récupérer les valeurs nettes et brutes pour une période donnée - (double, double)? _getStackedValuesForPeriod(BuildContext context, String periodKey) { - if (dataList.isEmpty || !(dataList.first is APYRecord)) { + (double, double)? _getStackedValuesForPeriod( + BuildContext context, String periodKey) { + if (dataList.isEmpty || dataList.first is! APYRecord) { return null; } - + // On filtre d'abord les données selon la plage de temps final filteredData = _filterDataByTimeRange(context, dataList); - + List periodRecords = []; - + for (var record in filteredData) { if (record is APYRecord) { DateTime date = record.timestamp; @@ -814,35 +853,36 @@ class GenericChartWidget extends StatelessWidget { if (selectedPeriod == S.of(context).day) { key = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - key = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + key = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { key = DateFormat('yyyy/MM').format(date); } else { key = date.year.toString(); } - + if (key == periodKey) { periodRecords.add(record); } } } - + if (periodRecords.isEmpty) { return null; } - + // Calculer les moyennes double netAvg = 0; double grossAvg = 0; - + for (var record in periodRecords) { netAvg += record.netApy ?? record.apy; grossAvg += record.grossApy ?? record.apy; } - + netAvg /= periodRecords.length; grossAvg /= periodRecords.length; - + return (netAvg, grossAvg); } @@ -857,8 +897,8 @@ class GenericChartWidget extends StatelessWidget { child: CupertinoSwitch( value: isCumulative!, onChanged: onCumulativeChanged!, - activeColor: Theme.of(context).primaryColor, - trackColor: Colors.grey.shade300, + activeTrackColor: Theme.of(context).primaryColor, + inactiveTrackColor: Colors.grey.shade300, ), ), const SizedBox(width: 8), @@ -869,7 +909,8 @@ class GenericChartWidget extends StatelessWidget { color: Theme.of(context).primaryColor, ), style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + backgroundColor: + Theme.of(context).primaryColor.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -883,7 +924,8 @@ class GenericChartWidget extends StatelessWidget { onTimeRangeChanged: onTimeRangeChanged, selectedPeriod: selectedPeriod, onPeriodChanged: onPeriodChanged, - onEditPressed: onEditPressed != null ? () => onEditPressed!(context) : null, + onEditPressed: + onEditPressed != null ? () => onEditPressed!(context) : null, ); }, ), @@ -901,9 +943,10 @@ class GenericChartWidget extends StatelessWidget { if (index % 2 == 1) { return const SizedBox(width: 24); // Espace entre les éléments } - + int itemIndex = index ~/ 2; - return _buildLegendItem(stackLabels![itemIndex], stackColors![itemIndex]); + return _buildLegendItem( + stackLabels![itemIndex], stackColors![itemIndex]); }), ), ); @@ -937,18 +980,19 @@ class GenericChartWidget extends StatelessWidget { List _buildStackedLineChartData(BuildContext context) { // On filtre d'abord les données selon la plage de temps final filteredData = _filterDataByTimeRange(context, dataList); - + if (filteredData.isEmpty || getStackValues == null || stackColors == null) { return []; } - + // Vérifier que nous avons suffisamment de couleurs final stackCount = getStackValues!(filteredData.first).length; if (stackCount != stackColors!.length) { - debugPrint("❌ Erreur: Le nombre de valeurs empilées ($stackCount) ne correspond pas au nombre de couleurs (${stackColors!.length})"); + debugPrint( + "❌ Erreur: Le nombre de valeurs empilées ($stackCount) ne correspond pas au nombre de couleurs (${stackColors!.length})"); return []; } - + // Regrouper les données selon la période sélectionnée Map> groupedData = {}; for (var record in filteredData) { @@ -958,7 +1002,8 @@ class GenericChartWidget extends StatelessWidget { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -970,20 +1015,20 @@ class GenericChartWidget extends StatelessWidget { // Trier les périodes List sortedKeys = groupedData.keys.toList()..sort(); - + // Créer les points pour chaque série avec les données regroupées List> stackedSpots = List.generate(stackCount, (_) => []); - + for (int i = 0; i < sortedKeys.length; i++) { String periodKey = sortedKeys[i]; List periodRecords = groupedData[periodKey]!; - + // Vérifier qu'il y a des enregistrements pour éviter une division par zéro if (periodRecords.isEmpty) continue; - + // Calculer les valeurs moyennes pour cette période List stackAverages = List.filled(stackCount, 0.0); - + for (var record in periodRecords) { try { final stackValues = getStackValues!(record); @@ -997,7 +1042,7 @@ class GenericChartWidget extends StatelessWidget { debugPrint("❌ Erreur lors de l'obtention des valeurs empilées: $e"); } } - + // Diviser par le nombre d'enregistrements pour obtenir la moyenne for (int j = 0; j < stackCount; j++) { stackAverages[j] /= periodRecords.length; @@ -1006,7 +1051,7 @@ class GenericChartWidget extends StatelessWidget { stackAverages[j] = 1.0; } } - + // Cumuler les valeurs pour l'empilement double cumulativeValue = 0; for (int j = 0; j < stackCount; j++) { @@ -1015,21 +1060,24 @@ class GenericChartWidget extends StatelessWidget { if (cumulativeValue.isNaN || cumulativeValue.isInfinite) { cumulativeValue = (j + 1) * 1.0; // Valeur de repli simple } - + // Ajouter le point avec validation double xValue = i.toDouble(); - if (!xValue.isNaN && !xValue.isInfinite && !cumulativeValue.isNaN && !cumulativeValue.isInfinite) { + if (!xValue.isNaN && + !xValue.isInfinite && + !cumulativeValue.isNaN && + !cumulativeValue.isInfinite) { stackedSpots[j].add(FlSpot(xValue, cumulativeValue)); } } } - + // Créer un LineChartBarData pour chaque série (en ordre inverse pour l'empilement visuel correct) final List lineBarsData = []; - + for (int i = stackCount - 1; i >= 0; i--) { final color = stackColors![i]; - + lineBarsData.add( LineChartBarData( spots: stackedSpots[i], @@ -1051,18 +1099,18 @@ class GenericChartWidget extends StatelessWidget { // Montrer points aux extrémités et quelques points intermédiaires final isFirst = spot.x == 0; final isLast = spot.x == barData.spots.length - 1; - + // Calcul du pas adaptatif pour l'affichage des points final int dataLength = barData.spots.length; int step = 1; - + // Si trop de points, n'afficher qu'un sous-ensemble if (dataLength > ChartUtils.maxBarsToDisplay) { step = (dataLength / ChartUtils.maxBarsToDisplay).ceil(); } else if (dataLength > 10) { step = 2; // Afficher un point sur deux si entre 10 et 20 points } - + final isInteresting = spot.x % step == 0; return isFirst || isLast || isInteresting; }, @@ -1071,8 +1119,8 @@ class GenericChartWidget extends StatelessWidget { show: true, gradient: LinearGradient( colors: [ - color.withOpacity(0.3), - color.withOpacity(0.05), + color.withValues(alpha: 0.3), + color.withValues(alpha: 0.05), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -1081,7 +1129,7 @@ class GenericChartWidget extends StatelessWidget { ), ); } - + return lineBarsData; } @@ -1105,7 +1153,7 @@ class GenericChartWidget extends StatelessWidget { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -1), ), @@ -1195,16 +1243,22 @@ class GenericChartWidget extends StatelessWidget { .asMap() .entries .toList() - ..sort((a, b) => getTimestamp(b.value).compareTo(getTimestamp(a.value))); - + ..sort((a, b) => getTimestamp(b.value) + .compareTo(getTimestamp(a.value))); + return sortedEntries.map((entry) { final index = entry.key; final record = entry.value; - TextEditingController valueController = TextEditingController( - text: getDisplayValue?.call(record) ?? getYValue(record).toStringAsFixed(2), + TextEditingController valueController = + TextEditingController( + text: getDisplayValue?.call(record) ?? + getYValue(record).toStringAsFixed(2), ); - TextEditingController dateController = TextEditingController( - text: getDisplayDate?.call(record) ?? DateFormat('yyyy-MM-dd HH:mm').format(getTimestamp(record)), + TextEditingController dateController = + TextEditingController( + text: getDisplayDate?.call(record) ?? + DateFormat('yyyy-MM-dd HH:mm') + .format(getTimestamp(record)), ); return DataRow( @@ -1219,7 +1273,9 @@ class GenericChartWidget extends StatelessWidget { style: TextStyle( fontSize: 14, ), - decoration: WidgetFactory.buildStandardInputDecoration(context), + decoration: WidgetFactory + .buildStandardInputDecoration( + context), onSubmitted: (value) { if (onDateChanged != null) { onDateChanged!(record, value); @@ -1233,15 +1289,19 @@ class GenericChartWidget extends StatelessWidget { width: 100, child: TextField( controller: valueController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType + .numberWithOptions(decimal: true), textInputAction: TextInputAction.done, inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d*')), ], style: TextStyle( fontSize: 14, ), - decoration: WidgetFactory.buildStandardInputDecoration(context), + decoration: WidgetFactory + .buildStandardInputDecoration( + context), onSubmitted: (value) { if (onValueChanged != null) { onValueChanged!(record, value); @@ -1254,13 +1314,16 @@ class GenericChartWidget extends StatelessWidget { SizedBox( width: 60, child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ IconButton( icon: Icon( Icons.delete_outline, color: Colors.red.shade700, - size: 20 + appState.getTextSizeOffset(), + size: 20 + + appState + .getTextSizeOffset(), ), onPressed: () { if (onDelete != null) { @@ -1286,4 +1349,4 @@ class GenericChartWidget extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/components/charts/pie_chart_builders.dart b/lib/components/charts/pie_chart_builders.dart index e9cb502..0740a68 100644 --- a/lib/components/charts/pie_chart_builders.dart +++ b/lib/components/charts/pie_chart_builders.dart @@ -7,7 +7,6 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; /// Factory pour construire les éléments de graphiques en secteurs de manière standardisée /// Réduit la duplication dans les cartes de distribution de tokens class PieChartBuilders { - /// Construit un graphique en secteurs standardisé avec gestion du touch static Widget buildStandardPieChart({ required BuildContext context, @@ -18,7 +17,7 @@ class PieChartBuilders { double sectionsSpace = 3, }) { final appState = Provider.of(context, listen: false); - + return Card( elevation: 0, shape: RoundedRectangleBorder( @@ -30,7 +29,7 @@ class PieChartBuilders { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -64,23 +63,30 @@ class PieChartBuilders { sectionsSpace: sectionsSpace, borderData: FlBorderData(show: false), pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, PieTouchResponse? response) { - if (response != null && response.touchedSection != null) { - final touchedIndex = response.touchedSection!.touchedSectionIndex; - selectedIndexNotifier.value = touchedIndex >= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), - if (selectedIndex != null && selectedIndex < sections.length) + if (selectedIndex != null && + selectedIndex < sections.length) buildCenterText( context: context, - title: _extractTitleFromSection(sections[selectedIndex]), + title: + _extractTitleFromSection(sections[selectedIndex]), value: sections[selectedIndex].value, appState: appState, ), @@ -113,11 +119,11 @@ class PieChartBuilders { final opacity = selectedIndex != null && !isSelected ? 0.5 : 1.0; final radius = isSelected ? selectedRadius : normalRadius; final fontSize = isSelected ? selectedFontSize : normalFontSize; - + return PieChartSectionData( value: value, title: title, - color: color.withOpacity(opacity), + color: color.withValues(alpha: opacity), radius: radius, titleStyle: TextStyle( fontSize: fontSize + appState.getTextSizeOffset(), @@ -125,13 +131,14 @@ class PieChartBuilders { fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), ], ), - badgeWidget: isSelected && showBadge ? buildSelectedIndicator(context) : null, + badgeWidget: + isSelected && showBadge ? buildSelectedIndicator(context) : null, badgePositionPercentageOffset: 1.1, ); } @@ -150,7 +157,7 @@ class PieChartBuilders { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: const Offset(0, 1), ), @@ -208,7 +215,7 @@ class PieChartBuilders { }) { final double othersPercentage = (othersValue / totalCount) * 100; final bool isOthersSelected = selectedIndex == sectionIndex; - + return buildStandardSection( value: othersValue, title: '${S.of(context).others} ${othersPercentage.toStringAsFixed(1)}%', @@ -245,9 +252,10 @@ class PieChartBuilders { /// Extrait le titre d'une section pour l'affichage central static String _extractTitleFromSection(PieChartSectionData section) { // Extraire le nom avant le pourcentage - final title = section.title ?? ''; + final title = section.title; final percentageIndex = title.lastIndexOf(' '); - if (percentageIndex > 0 && title.substring(percentageIndex + 1).contains('%')) { + if (percentageIndex > 0 && + title.substring(percentageIndex + 1).contains('%')) { return title.substring(0, percentageIndex); } return title; @@ -262,11 +270,13 @@ class PieChartBuilders { return Wrap( spacing: 16, runSpacing: 8, - children: items.map((item) => buildLegendItem( - label: item.label, - color: item.color, - appState: appState, - )).toList(), + children: items + .map((item) => buildLegendItem( + label: item.label, + color: item.color, + appState: appState, + )) + .toList(), ); } @@ -309,4 +319,4 @@ class LegendItem { required this.label, required this.color, }); -} \ No newline at end of file +} diff --git a/lib/components/donation_card_widget.dart b/lib/components/donation_card_widget.dart index 5645463..80ade6e 100644 --- a/lib/components/donation_card_widget.dart +++ b/lib/components/donation_card_widget.dart @@ -6,12 +6,9 @@ import 'package:realtoken_asset_tracker/components/donation_widgets.dart'; class DonationCardWidget extends StatelessWidget { final String? montantWallet; final bool isLoading; - - const DonationCardWidget({ - Key? key, - this.montantWallet, - this.isLoading = false - }) : super(key: key); + + const DonationCardWidget( + {super.key, this.montantWallet, this.isLoading = false}); @override Widget build(BuildContext context) { @@ -27,4 +24,4 @@ class DonationCardWidget extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/components/donation_widgets.dart b/lib/components/donation_widgets.dart index 422dea2..0ce8b8a 100644 --- a/lib/components/donation_widgets.dart +++ b/lib/components/donation_widgets.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; @@ -9,18 +8,19 @@ import 'package:provider/provider.dart'; /// Factory pour créer tous les widgets de donation de manière uniforme class DonationWidgets { - /// Constantes de couleurs pour les boutons de donation static const Color buyMeACoffeeColor = Color(0xFFFFDD00); static const Color paypalColor = Color(0xFF0070ba); static const Color cryptoBackgroundLight = Color(0xFFF4F4F4); - + /// Adresse de donation crypto - static const String donationAddress = '0xdc30b07aebaef3f15544a3801c6cb0f35f0118fc'; - + static const String donationAddress = + '0xdc30b07aebaef3f15544a3801c6cb0f35f0118fc'; + /// URLs de donation static const String buyMeACoffeeUrl = 'https://buymeacoffee.com/byackee'; - static const String paypalUrl = 'https://paypal.me/byackee?country.x=FR&locale.x=fr_FR'; + static const String paypalUrl = + 'https://paypal.me/byackee?country.x=FR&locale.x=fr_FR'; /// Construit le widget d'affichage du montant de donation avec loading static Widget buildAmountDisplay({ @@ -31,7 +31,8 @@ class DonationWidgets { }) { if (isLoading) { return Padding( - padding: EdgeInsets.symmetric(vertical: 16 + appState.getTextSizeOffset()), + padding: + EdgeInsets.symmetric(vertical: 16 + appState.getTextSizeOffset()), child: SizedBox( width: 36, height: 36, @@ -50,7 +51,11 @@ class DonationWidgets { S.of(context).donationTotal, style: TextStyle( fontSize: 15 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.8), ), ), SizedBox(height: 4 + appState.getTextSizeOffset()), @@ -68,7 +73,8 @@ class DonationWidgets { ), SizedBox(width: 6 + appState.getTextSizeOffset()), Padding( - padding: EdgeInsets.only(bottom: 2 + appState.getTextSizeOffset()), + padding: + EdgeInsets.only(bottom: 2 + appState.getTextSizeOffset()), child: Text( 'USD', style: TextStyle( @@ -94,7 +100,7 @@ class DonationWidgets { required AppState appState, }) { return Text( - S.of(context).supportProject + " ❤️", + "${S.of(context).supportProject} ❤️", style: TextStyle( fontSize: 24 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -145,7 +151,8 @@ class DonationWidgets { vertical: 10 + appState.getTextSizeOffset(), ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(StyleConstants.buttonBorderRadius), + borderRadius: + BorderRadius.circular(StyleConstants.buttonBorderRadius), ), elevation: 0, ), @@ -180,7 +187,8 @@ class DonationWidgets { vertical: 10 + appState.getTextSizeOffset(), ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(StyleConstants.buttonBorderRadius), + borderRadius: + BorderRadius.circular(StyleConstants.buttonBorderRadius), ), elevation: 0, ), @@ -199,7 +207,9 @@ class DonationWidgets { Text( S.of(context).cryptoDonation, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Theme.of(context).textTheme.bodySmall?.color, ), ), @@ -210,7 +220,8 @@ class DonationWidgets { color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[800] : cryptoBackgroundLight, - borderRadius: BorderRadius.circular(StyleConstants.smallBorderRadius), + borderRadius: + BorderRadius.circular(StyleConstants.smallBorderRadius), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -229,7 +240,8 @@ class DonationWidgets { icon: Icon( Icons.copy_rounded, size: 20, - color: Theme.of(context).iconTheme.color?.withOpacity(0.7), + color: + Theme.of(context).iconTheme.color?.withValues(alpha: 0.7), ), tooltip: S.of(context).copy, onPressed: () => _copyAddressToClipboard(context), @@ -249,8 +261,13 @@ class DonationWidgets { return Text( S.of(context).everyContributionCounts, style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.7), + fontSize: 12 + + Provider.of(context, listen: false).getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color + ?.withValues(alpha: 0.7), ), textAlign: TextAlign.center, ); @@ -292,7 +309,6 @@ class DonationWidgets { buildTitle(context: context, appState: appState), buildSpacer(appState: appState, baseHeight: 4), ], - if (showAmount) buildAmountDisplay( context: context, @@ -300,25 +316,21 @@ class DonationWidgets { amount: amount, isLoading: isLoading, ), - buildSpacer(appState: appState), buildDescription(context: context, appState: appState), buildSpacer(appState: appState), buildDivider(context: context), buildSpacer(appState: appState), - if (showButtons) ...[ buildBuyMeACoffeeButton(context: context, appState: appState), buildSpacer(appState: appState, baseHeight: 10), buildPaypalButton(context: context, appState: appState), buildSpacer(appState: appState, baseHeight: 10), ], - if (showCrypto) ...[ buildCryptoSection(context: context, appState: appState), buildSpacer(appState: appState), ], - if (showThankYou) buildThankYouMessage(context: context, appState: appState), ], @@ -337,4 +349,4 @@ class DonationWidgets { ); } } -} \ No newline at end of file +} diff --git a/lib/components/drawer_menu_factory.dart b/lib/components/drawer_menu_factory.dart index e5e1746..1ee9b73 100644 --- a/lib/components/drawer_menu_factory.dart +++ b/lib/components/drawer_menu_factory.dart @@ -12,7 +12,9 @@ class DrawerMenuFactory { required List items, }) { return Container( - margin: const EdgeInsets.symmetric(horizontal: StyleConstants.marginLarge, vertical: StyleConstants.marginSmall), + margin: const EdgeInsets.symmetric( + horizontal: StyleConstants.marginLarge, + vertical: StyleConstants.marginSmall), decoration: StyleConstants.cardDecoration(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -28,14 +30,15 @@ class DrawerMenuFactory { isFirst: index == 0, isLast: index == items.length - 1, ); - }).toList(), + }), ], ), ); } /// Crée un titre de section - static Widget _buildSectionTitle(BuildContext context, String title, AppState appState) { + static Widget _buildSectionTitle( + BuildContext context, String title, AppState appState) { return Padding( padding: const EdgeInsets.fromLTRB( StyleConstants.paddingMedium, @@ -45,7 +48,8 @@ class DrawerMenuFactory { ), child: Text( title, - style: StyleConstants.titleStyle(context, appState.getTextSizeOffset()).copyWith( + style: StyleConstants.titleStyle(context, appState.getTextSizeOffset()) + .copyWith( fontSize: 18 + appState.getTextSizeOffset(), color: Theme.of(context).primaryColor, ), @@ -67,7 +71,8 @@ class DrawerMenuFactory { color: Colors.transparent, child: InkWell( onTap: item.onTap, - borderRadius: BorderRadius.circular(StyleConstants.buttonBorderRadius), + borderRadius: + BorderRadius.circular(StyleConstants.buttonBorderRadius), child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleConstants.paddingLarge, @@ -80,7 +85,9 @@ class DrawerMenuFactory { Expanded( child: Text( item.title, - style: StyleConstants.bodyStyle(context, appState.getTextSizeOffset()).copyWith( + style: StyleConstants.bodyStyle( + context, appState.getTextSizeOffset()) + .copyWith( fontWeight: FontWeight.w400, color: CupertinoColors.label.resolveFrom(context), ), @@ -107,7 +114,8 @@ class DrawerMenuFactory { width: 32, height: 32, decoration: BoxDecoration( - color: (item.iconColor ?? Theme.of(context).primaryColor).withOpacity(0.1), + color: (item.iconColor ?? Theme.of(context).primaryColor) + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(StyleConstants.buttonBorderRadius), ), child: Icon( @@ -181,13 +189,17 @@ class DrawerMenuFactory { showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: CupertinoColors.systemBackground.resolveFrom(context), + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(StyleConstants.modalBorderRadius + 4)), + borderRadius: BorderRadius.vertical( + top: Radius.circular(StyleConstants.modalBorderRadius + 4)), ), builder: (BuildContext context) { return SizedBox( - height: modalHeight ?? MediaQuery.of(context).size.height * StyleConstants.maxModalHeight, + height: modalHeight ?? + MediaQuery.of(context).size.height * + StyleConstants.maxModalHeight, child: modal, ); }, @@ -210,4 +222,4 @@ class DrawerMenuItem { required this.onTap, this.iconColor, }); -} \ No newline at end of file +} diff --git a/lib/components/filter_widgets.dart b/lib/components/filter_widgets.dart index 171d627..8b28e63 100644 --- a/lib/components/filter_widgets.dart +++ b/lib/components/filter_widgets.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; /// Factory pour construire les widgets de filtres de manière standardisée /// Réduit la duplication entre portfolio_page.dart et realtokens_page.dart class FilterWidgets { - /// Construit un bouton de filtre simple standardisé static Widget buildFilterButton({ required BuildContext context, @@ -26,7 +23,7 @@ class FilterWidgets { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -55,7 +52,7 @@ class FilterWidgets { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -98,15 +95,14 @@ class FilterWidgets { .map((item) { String country = item['country'] ?? "Unknown Country"; // Regrouper les tokens factoring_profitshare avec des séries sous "Series XX" - if ((item['productType']?.toString().toLowerCase() == 'factoring_profitshare') && + if ((item['productType']?.toString().toLowerCase() == + 'factoring_profitshare') && country.toLowerCase().startsWith('series ')) { return "Series XX"; } return country; }) - .where((country) => country != null) .toSet() - .cast() .toList(); countries.sort(); return countries; @@ -114,52 +110,46 @@ class FilterWidgets { /// Construit un PopupMenuItem pour les villes avec l'option "Toutes les villes" static List> buildCityMenuItems( - BuildContext context, - List cities - ) { + BuildContext context, List cities) { return [ PopupMenuItem( value: S.of(context).allCities, child: Text(S.of(context).allCities), ), ...cities.map((city) => PopupMenuItem( - value: city, - child: Text(city), - )), + value: city, + child: Text(city), + )), ]; } /// Construit un PopupMenuItem pour les régions avec l'option "Toutes les régions" static List> buildRegionMenuItems( - BuildContext context, - List regions - ) { + BuildContext context, List regions) { return [ PopupMenuItem( value: S.of(context).allRegions, child: Text(S.of(context).allRegions), ), ...regions.map((region) => PopupMenuItem( - value: region, - child: Text(region), - )), + value: region, + child: Text(region), + )), ]; } /// Construit un PopupMenuItem pour les pays avec l'option "Tous les pays" static List> buildCountryMenuItems( - BuildContext context, - List countries - ) { + BuildContext context, List countries) { return [ PopupMenuItem( value: S.of(context).allCountries, child: Text(S.of(context).allCountries), ), ...countries.map((country) => PopupMenuItem( - value: country, - child: Text(country), - )), + value: country, + child: Text(country), + )), ]; } @@ -216,4 +206,4 @@ class FilterWidgets { }, ); } -} \ No newline at end of file +} diff --git a/lib/components/info_card_widgets.dart b/lib/components/info_card_widgets.dart index 6895f25..207eb59 100644 --- a/lib/components/info_card_widgets.dart +++ b/lib/components/info_card_widgets.dart @@ -7,19 +7,18 @@ import 'package:provider/provider.dart'; /// Factory pour créer des cartes d'information standardisées class InfoCardWidgets { - /// Construit un header de section standardisé static Widget buildSectionHeader( - BuildContext context, - String title, - double textSizeOffset - ) { + BuildContext context, String title, double textSizeOffset) { return Padding( padding: const EdgeInsets.only(top: 20.0, bottom: 12.0, left: 4.0), child: Text( title, style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, fontWeight: FontWeight.bold, color: CupertinoColors.label.resolveFrom(context), ), @@ -49,7 +48,8 @@ class InfoCardWidgets { _buildIconContainer(context, icon, iconColor), const SizedBox(width: 14), Expanded( - child: _buildTextContent(context, title, subtitle, textSizeOffset), + child: + _buildTextContent(context, title, subtitle, textSizeOffset), ), if (linkUrl != null || onTap != null) Icon( @@ -97,14 +97,12 @@ class InfoCardWidgets { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildIconContainer( - context, - CupertinoIcons.heart_fill, - CupertinoColors.systemPink.resolveFrom(context) - ), + _buildIconContainer(context, CupertinoIcons.heart_fill, + CupertinoColors.systemPink.resolveFrom(context)), const SizedBox(width: 14), Expanded( - child: _buildTextContent(context, title, subtitle, textSizeOffset), + child: + _buildTextContent(context, title, subtitle, textSizeOffset), ), ], ), @@ -140,7 +138,10 @@ class InfoCardWidgets { Text( title, style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, fontWeight: FontWeight.w500, color: CupertinoColors.label.resolveFrom(context), ), @@ -149,7 +150,10 @@ class InfoCardWidgets { Text( url, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, color: CupertinoColors.systemBlue.resolveFrom(context), ), ), @@ -180,7 +184,8 @@ class InfoCardWidgets { Color? cardColor, }) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final finalCardColor = cardColor ?? (isDarkMode ? const Color(0xFF2C2C2E) : Colors.white); + final finalCardColor = + cardColor ?? (isDarkMode ? const Color(0xFF2C2C2E) : Colors.white); return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), @@ -201,7 +206,10 @@ class InfoCardWidgets { Text( value, style: TextStyle( - fontSize: 22 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 22 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, fontWeight: FontWeight.bold, color: valueColor ?? (isDarkMode ? Colors.white : Colors.black), ), @@ -210,7 +218,10 @@ class InfoCardWidgets { Text( title, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, color: isDarkMode ? Colors.white70 : Colors.black54, ), ), @@ -236,16 +247,16 @@ class InfoCardWidgets { children: [ Row( children: [ - _buildIconContainer( - context, - CupertinoIcons.money_dollar_circle, - CupertinoColors.systemGreen.resolveFrom(context) - ), + _buildIconContainer(context, CupertinoIcons.money_dollar_circle, + CupertinoColors.systemGreen.resolveFrom(context)), const SizedBox(width: 12), Text( title, style: TextStyle( - fontSize: 17 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 17 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, fontWeight: FontWeight.w600, color: CupertinoColors.label.resolveFrom(context), ), @@ -258,7 +269,10 @@ class InfoCardWidgets { child: Text( subtitle, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), @@ -272,7 +286,8 @@ class InfoCardWidgets { // --- MÉTHODES PRIVÉES UTILITAIRES --- /// Construit le container d'icône standardisé - static Widget _buildIconContainer(BuildContext context, IconData icon, Color? iconColor) { + static Widget _buildIconContainer( + BuildContext context, IconData icon, Color? iconColor) { return Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( @@ -288,19 +303,18 @@ class InfoCardWidgets { } /// Construit le contenu textuel standardisé (titre + sous-titre) - static Widget _buildTextContent( - BuildContext context, - String title, - String subtitle, - double textSizeOffset - ) { + static Widget _buildTextContent(BuildContext context, String title, + String subtitle, double textSizeOffset) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, fontWeight: FontWeight.w500, color: CupertinoColors.label.resolveFrom(context), ), @@ -309,11 +323,14 @@ class InfoCardWidgets { Text( subtitle, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset() + + textSizeOffset, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), ], ); } -} \ No newline at end of file +} diff --git a/lib/components/section_card_widget.dart b/lib/components/section_card_widget.dart index fcefda1..baf4229 100644 --- a/lib/components/section_card_widget.dart +++ b/lib/components/section_card_widget.dart @@ -16,7 +16,7 @@ class SectionCardWidget extends StatelessWidget { final FontWeight? titleFontWeight; const SectionCardWidget({ - Key? key, + super.key, required this.title, required this.children, this.margin, @@ -25,12 +25,12 @@ class SectionCardWidget extends StatelessWidget { this.titleColor, this.titleFontSize, this.titleFontWeight, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final appState = Provider.of(context, listen: false); - + return Container( margin: margin ?? const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( @@ -42,7 +42,8 @@ class SectionCardWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: titlePadding ?? const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 6.0), + padding: titlePadding ?? + const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 6.0), child: Text( title, style: TextStyle( @@ -80,7 +81,7 @@ class DetailRowWidget extends StatelessWidget { final FontWeight? valueFontWeight; const DetailRowWidget({ - Key? key, + super.key, required this.label, required this.value, this.icon, @@ -94,14 +95,15 @@ class DetailRowWidget extends StatelessWidget { this.valueFontSize, this.labelFontWeight, this.valueFontWeight, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final appState = Provider.of(context, listen: false); return Padding( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + padding: padding ?? + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -118,7 +120,8 @@ class DetailRowWidget extends StatelessWidget { width: 32, height: 32, decoration: BoxDecoration( - color: (iconColor ?? Colors.blue).withOpacity(0.1), + color: + (iconColor ?? Colors.blue).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -131,7 +134,8 @@ class DetailRowWidget extends StatelessWidget { Text( label, style: TextStyle( - fontSize: (labelFontSize ?? 14) + appState.getTextSizeOffset(), + fontSize: + (labelFontSize ?? 14) + appState.getTextSizeOffset(), fontWeight: labelFontWeight ?? FontWeight.w300, color: Theme.of(context).textTheme.bodyMedium?.color, ), @@ -144,8 +148,8 @@ class DetailRowWidget extends StatelessWidget { style: TextStyle( fontSize: (valueFontSize ?? 14) + appState.getTextSizeOffset(), fontWeight: valueFontWeight ?? FontWeight.w400, - color: isExpenseItem - ? Colors.red + color: isExpenseItem + ? Colors.red : (textColor ?? Theme.of(context).textTheme.bodyLarge?.color), ), ), @@ -153,4 +157,4 @@ class DetailRowWidget extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/components/wallet_popup_widget.dart b/lib/components/wallet_popup_widget.dart index 9669d4f..45a61a0 100644 --- a/lib/components/wallet_popup_widget.dart +++ b/lib/components/wallet_popup_widget.dart @@ -15,21 +15,24 @@ class WalletPopupWidget extends StatelessWidget { final appState = Provider.of(context, listen: false); final dataManager = Provider.of(context, listen: false); - final List> walletDetails = dataManager.perWalletBalances ?? []; - final bool hasStaking = walletDetails.any((wallet) => (wallet['gnosisVaultReg'] ?? 0) > 0); + final List> walletDetails = + dataManager.perWalletBalances; + final bool hasStaking = + walletDetails.any((wallet) => (wallet['gnosisVaultReg'] ?? 0) > 0); return BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.9) - : Colors.white.withOpacity(0.9), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.9) + : Colors.white.withValues(alpha: 0.9), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, spreadRadius: 0, ), @@ -43,7 +46,8 @@ class WalletPopupWidget extends StatelessWidget { children: [ _buildDragIndicator(context), _buildHeader(context, appState), - _buildTotalBalanceCard(context, appState, currencyUtils, dataManager, hasStaking), + _buildTotalBalanceCard( + context, appState, currencyUtils, dataManager, hasStaking), _buildWalletDetailsHeader(context, appState), _buildWalletList(context, walletDetails, currencyUtils, appState), ], @@ -58,9 +62,9 @@ class WalletPopupWidget extends StatelessWidget { width: 36, height: 4, decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), ); @@ -98,15 +102,20 @@ class WalletPopupWidget extends StatelessWidget { ); } - Widget _buildTotalBalanceCard(BuildContext context, AppState appState, CurrencyProvider currencyUtils, DataManager dataManager, bool hasStaking) { + Widget _buildTotalBalanceCard( + BuildContext context, + AppState appState, + CurrencyProvider currencyUtils, + DataManager dataManager, + bool hasStaking) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), border: Border.all( - color: Theme.of(context).primaryColor.withOpacity(0.2), + color: Theme.of(context).primaryColor.withValues(alpha: 0.2), width: 1, ), ), @@ -125,9 +134,28 @@ class WalletPopupWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildCurrencyBalance(context, appState, currencyUtils, 'assets/icons/usdc.png', 'USDC', dataManager.gnosisUsdcBalance), - _buildCurrencyBalance(context, appState, currencyUtils, 'assets/icons/xdai.png', 'XDAI', dataManager.gnosisXdaiBalance), - _buildRegBalance(context, appState, 'assets/icons/reg.png', 'REG', dataManager.gnosisRegBalance + dataManager.gnosisVaultRegBalance, hasStaking), + _buildCurrencyBalance( + context, + appState, + currencyUtils, + 'assets/icons/usdc.png', + 'USDC', + dataManager.gnosisUsdcBalance), + _buildCurrencyBalance( + context, + appState, + currencyUtils, + 'assets/icons/xdai.png', + 'XDAI', + dataManager.gnosisXdaiBalance), + _buildRegBalance( + context, + appState, + 'assets/icons/reg.png', + 'REG', + dataManager.gnosisRegBalance + + dataManager.gnosisVaultRegBalance, + hasStaking), ], ), ], @@ -135,10 +163,18 @@ class WalletPopupWidget extends StatelessWidget { ); } - Widget _buildCurrencyBalance(BuildContext context, AppState appState, CurrencyProvider currencyUtils, String iconPath, String currency, double balance) { + Widget _buildCurrencyBalance( + BuildContext context, + AppState appState, + CurrencyProvider currencyUtils, + String iconPath, + String currency, + double balance) { return Row( children: [ - Image.asset(iconPath, width: 28 + appState.getTextSizeOffset(), height: 28 + appState.getTextSizeOffset()), + Image.asset(iconPath, + width: 28 + appState.getTextSizeOffset(), + height: 28 + appState.getTextSizeOffset()), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -147,11 +183,16 @@ class WalletPopupWidget extends StatelessWidget { currency, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), ), ), Text( - currencyUtils.formatCurrency(currencyUtils.convert(balance), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(balance), currencyUtils.currencySymbol), style: TextStyle( fontSize: 16 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -164,10 +205,13 @@ class WalletPopupWidget extends StatelessWidget { ); } - Widget _buildRegBalance(BuildContext context, AppState appState, String iconPath, String currency, double balance, bool hasStaking) { + Widget _buildRegBalance(BuildContext context, AppState appState, + String iconPath, String currency, double balance, bool hasStaking) { return Row( children: [ - Image.asset(iconPath, width: 28 + appState.getTextSizeOffset(), height: 28 + appState.getTextSizeOffset()), + Image.asset(iconPath, + width: 28 + appState.getTextSizeOffset(), + height: 28 + appState.getTextSizeOffset()), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -176,7 +220,11 @@ class WalletPopupWidget extends StatelessWidget { currency, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), ), ), Row( @@ -223,7 +271,11 @@ class WalletPopupWidget extends StatelessWidget { ); } - Widget _buildWalletList(BuildContext context, List> walletDetails, CurrencyProvider currencyUtils, AppState appState) { + Widget _buildWalletList( + BuildContext context, + List> walletDetails, + CurrencyProvider currencyUtils, + AppState appState) { return Expanded( child: ListView.builder( padding: const EdgeInsets.fromLTRB(12, 4, 12, 16), @@ -239,7 +291,7 @@ class WalletPopupWidget extends StatelessWidget { String _truncateAddress(String address) { if (address.length <= 12) return address; - return address.substring(0, 6) + "..." + address.substring(address.length - 4); + return "${address.substring(0, 6)}...${address.substring(address.length - 4)}"; } } @@ -264,14 +316,14 @@ class WalletItemWidget extends StatelessWidget { return Container( margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.05) - : Colors.black.withOpacity(0.03), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.03), borderRadius: BorderRadius.circular(14), border: Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.05), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.05), width: 1, ), ), @@ -295,7 +347,7 @@ class WalletItemWidget extends StatelessWidget { Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( @@ -320,7 +372,11 @@ class WalletItemWidget extends StatelessWidget { icon: Icon( Icons.copy_rounded, size: 14 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.6), ), constraints: const BoxConstraints(), padding: EdgeInsets.zero, @@ -336,17 +392,22 @@ class WalletItemWidget extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildCurrencyInfo(context, 'assets/icons/usdc.png', 'USDC', _getFormattedAmount('gnosisUsdc')), - _buildCurrencyInfo(context, 'assets/icons/xdai.png', 'XDAI', _getFormattedAmount('gnosisXdai')), + _buildCurrencyInfo(context, 'assets/icons/usdc.png', 'USDC', + _getFormattedAmount('gnosisUsdc')), + _buildCurrencyInfo(context, 'assets/icons/xdai.png', 'XDAI', + _getFormattedAmount('gnosisXdai')), _buildRegInfo(context), ], ); } - Widget _buildCurrencyInfo(BuildContext context, String iconPath, String currency, String amount) { + Widget _buildCurrencyInfo( + BuildContext context, String iconPath, String currency, String amount) { return Row( children: [ - Image.asset(iconPath, width: 20 + appState.getTextSizeOffset(), height: 20 + appState.getTextSizeOffset()), + Image.asset(iconPath, + width: 20 + appState.getTextSizeOffset(), + height: 20 + appState.getTextSizeOffset()), const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -355,7 +416,11 @@ class WalletItemWidget extends StatelessWidget { currency, style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), ), ), Text( @@ -375,11 +440,14 @@ class WalletItemWidget extends StatelessWidget { Widget _buildRegInfo(BuildContext context) { final double gnosisReg = walletInfo['gnosisReg']; final double gnosisVaultReg = walletInfo['gnosisVaultReg']; - final String gnosisRegTotal = (gnosisReg + gnosisVaultReg).toStringAsFixed(2); + final String gnosisRegTotal = + (gnosisReg + gnosisVaultReg).toStringAsFixed(2); return Row( children: [ - Image.asset('assets/icons/reg.png', width: 20 + appState.getTextSizeOffset(), height: 20 + appState.getTextSizeOffset()), + Image.asset('assets/icons/reg.png', + width: 20 + appState.getTextSizeOffset(), + height: 20 + appState.getTextSizeOffset()), const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -388,7 +456,11 @@ class WalletItemWidget extends StatelessWidget { "REG", style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), ), ), Row( @@ -428,6 +500,6 @@ class WalletItemWidget extends StatelessWidget { String _truncateAddress(String address) { if (address.length <= 12) return address; - return address.substring(0, 6) + "..." + address.substring(address.length - 4); + return "${address.substring(0, 6)}...${address.substring(address.length - 4)}"; } -} \ No newline at end of file +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 4e60207..638c3f0 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,7 +1,8 @@ // File generated by FlutterFire CLI. // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -53,7 +54,8 @@ class DefaultFirebaseOptions { messagingSenderId: '203512481394', projectId: 'realtoken-88d99', storageBucket: 'realtoken-88d99.firebasestorage.app', - iosClientId: '203512481394-6jfun8uvjj28nrdpg166j4k7htundoej.apps.googleusercontent.com', + iosClientId: + '203512481394-6jfun8uvjj28nrdpg166j4k7htundoej.apps.googleusercontent.com', iosBundleId: 'com.example.realtApps', ); @@ -63,7 +65,8 @@ class DefaultFirebaseOptions { messagingSenderId: '203512481394', projectId: 'realtoken-88d99', storageBucket: 'realtoken-88d99.firebasestorage.app', - iosClientId: '203512481394-6jfun8uvjj28nrdpg166j4k7htundoej.apps.googleusercontent.com', + iosClientId: + '203512481394-6jfun8uvjj28nrdpg166j4k7htundoej.apps.googleusercontent.com', iosBundleId: 'com.example.realtApps', ); diff --git a/lib/main.dart b/lib/main.dart index 6bff7ca..d473c11 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,8 +24,6 @@ import 'managers/apy_manager.dart'; import 'screens/lock_screen.dart'; import 'utils/data_fetch_utils.dart'; import 'utils/preference_keys.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); @@ -40,7 +38,8 @@ void main() async { try { if (Firebase.apps.isEmpty) { - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform); debugPrint("✅ Firebase initialisé !"); } } catch (e, stacktrace) { @@ -89,7 +88,9 @@ void main() async { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => dataManager), - ChangeNotifierProvider(create: (_) => CurrencyProvider()), // ✅ Assurez-vous que CurrencyProvider est bien ici + ChangeNotifierProvider( + create: (_) => + CurrencyProvider()), // ✅ Assurez-vous que CurrencyProvider est bien ici ChangeNotifierProvider(create: (_) => appState), ], child: MyApp(autoSyncEnabled: autoSyncEnabled), @@ -125,10 +126,10 @@ class MyAppState extends State with WidgetsBindingObserver { _checkAuthentication(); _checkGoogleDriveConnection(); _autoSyncEnabled = widget.autoSyncEnabled; - + // Charger les données initiales de l'application _loadInitialData(); - + if (!kIsWeb) { initOneSignal(); } else { @@ -205,31 +206,37 @@ class MyAppState extends State with WidgetsBindingObserver { OneSignal.Debug.setAlertLevel(OSLogLevel.none); OneSignal.consentRequired(_requireConsent); OneSignal.initialize("e7059f66-9c12-4d21-a078-edaf1a203dea"); - + // Vérifier si l'utilisateur a déjà refusé les notifications _checkAndRequestNotificationPermission(); - + OneSignal.Notifications.addForegroundWillDisplayListener((event) { - debugPrint('Notification reçue en premier plan : ${event.notification.jsonRepresentation()}'); + debugPrint( + 'Notification reçue en premier plan : ${event.notification.jsonRepresentation()}'); event.preventDefault(); event.notification.display(); }); OneSignal.Notifications.addClickListener((event) { - debugPrint('Notification cliquée : ${event.notification.jsonRepresentation()}'); + debugPrint( + 'Notification cliquée : ${event.notification.jsonRepresentation()}'); }); OneSignal.User.pushSubscription.addObserver((state) { - debugPrint('Utilisateur inscrit aux notifications : ${state.current.jsonRepresentation()}'); + debugPrint( + 'Utilisateur inscrit aux notifications : ${state.current.jsonRepresentation()}'); }); } Future _checkAndRequestNotificationPermission() async { final prefs = await SharedPreferences.getInstance(); - final hasRefusedNotifications = prefs.getBool(PreferenceKeys.hasRefusedNotifications) ?? false; - final hasAskedNotifications = prefs.getBool(PreferenceKeys.hasAskedNotifications) ?? false; + final hasRefusedNotifications = + prefs.getBool(PreferenceKeys.hasRefusedNotifications) ?? false; + final hasAskedNotifications = + prefs.getBool(PreferenceKeys.hasAskedNotifications) ?? false; // Si l'utilisateur a déjà refusé, ne pas redemander if (hasRefusedNotifications) { - debugPrint("🚫 L'utilisateur a déjà refusé les notifications, pas de nouvelle demande"); + debugPrint( + "🚫 L'utilisateur a déjà refusé les notifications, pas de nouvelle demande"); return; } @@ -237,13 +244,15 @@ class MyAppState extends State with WidgetsBindingObserver { if (!hasAskedNotifications) { debugPrint("📱 Première demande d'autorisation de notifications"); await prefs.setBool(PreferenceKeys.hasAskedNotifications, true); - - final hasPermission = await OneSignal.Notifications.requestPermission(true); - + + final hasPermission = + await OneSignal.Notifications.requestPermission(true); + // Si la permission a été refusée, sauvegarder cette information if (!hasPermission) { await prefs.setBool(PreferenceKeys.hasRefusedNotifications, true); - debugPrint("🚫 Permissions de notifications refusées par l'utilisateur"); + debugPrint( + "🚫 Permissions de notifications refusées par l'utilisateur"); } else { debugPrint("✅ Permissions de notifications accordées"); // Réinitialiser le flag de refus au cas où l'utilisateur accepterait après avoir refusé @@ -267,9 +276,12 @@ class MyAppState extends State with WidgetsBindingObserver { if (state == AppLifecycleState.resumed) { // Demander l'authentification UNIQUEMENT si l'application était en arrière-plan (inactive ou paused) // et qu'elle revient au premier plan après un certain temps - if (previousState == AppLifecycleState.paused || previousState == AppLifecycleState.inactive) { + if (previousState == AppLifecycleState.paused || + previousState == AppLifecycleState.inactive) { final now = DateTime.now(); - final needsAuth = _lastAuthTime == null || now.difference(_lastAuthTime!).inMinutes >= 5; // Redemander après 5 minutes + final needsAuth = _lastAuthTime == null || + now.difference(_lastAuthTime!).inMinutes >= + 5; // Redemander après 5 minutes if (needsAuth) { _checkAuthentication(); @@ -291,7 +303,7 @@ class MyAppState extends State with WidgetsBindingObserver { } catch (e) { debugPrint("❌ Erreur lors de la mise à jour des données: $e"); } - + await _loadAutoSyncPreference(); if (_autoSyncEnabled) { diff --git a/lib/managers/apy_manager.dart b/lib/managers/apy_manager.dart index 5064d1c..bc1d72d 100644 --- a/lib/managers/apy_manager.dart +++ b/lib/managers/apy_manager.dart @@ -1,5 +1,4 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:realtoken_asset_tracker/models/balance_record.dart'; import 'package:realtoken_asset_tracker/models/apy_record.dart'; @@ -9,7 +8,8 @@ import 'package:realtoken_asset_tracker/models/apy_record.dart'; class ApyManager extends ChangeNotifier { // Facteur d'alpha pour la moyenne mobile exponentielle (EMA) // Plus ce facteur est élevé (max 1.0), plus les valeurs récentes ont du poids - double emaAlpha = 0.2; // Valeur par défaut, ajustable selon les besoins de réactivité + double emaAlpha = + 0.2; // Valeur par défaut, ajustable selon les besoins de réactivité // Période maximale à considérer pour le calcul de l'APY (en jours) int maxHistoryDays = 30; // Valeur par défaut, ajustable @@ -36,31 +36,29 @@ class ApyManager extends ChangeNotifier { // Vérifier les valeurs invalides if (initialBalance <= 0 || finalBalance <= 0) { - return 0; } // Calculer la différence en pourcentage - double percentageChange = ((finalBalance - initialBalance) / initialBalance) * 100; + double percentageChange = + ((finalBalance - initialBalance) / initialBalance) * 100; // Ignorer si la différence est trop faible ou nulle (seuil réduit à 0.00001%) if (percentageChange.abs() < 0.00001) { - return 0; // Ne pas prendre en compte cette paire } // Ignorer si la différence est supérieure à 25% ou inférieure à 0% (dépôt ou retrait) if (percentageChange > 25 || percentageChange < 0) { - return 0; // Ne pas prendre en compte cette paire } // Calculer la durée en secondes - double timePeriodInSeconds = current.timestamp.difference(previous.timestamp).inSeconds.toDouble(); + double timePeriodInSeconds = + current.timestamp.difference(previous.timestamp).inSeconds.toDouble(); // Si la durée est trop courte (moins d'une minute), ignorer la paire if (timePeriodInSeconds < 60) { - return 0; } @@ -69,28 +67,26 @@ class ApyManager extends ChangeNotifier { // Si la période est trop courte, cela peut causer des NaN dans le calcul if (timePeriodInYears <= 0) { - return 0; } // Calculer l'APY annualisé double apy; try { - apy = (math.pow((1 + percentageChange / 100), (1 / timePeriodInYears)) - 1) * 100; + apy = (math.pow((1 + percentageChange / 100), (1 / timePeriodInYears)) - + 1) * + 100; } catch (e) { - return 0; } // Vérifier si le résultat est NaN if (apy.isNaN) { - return 0; } // Vérifier que l'APY calculé est dans les limites acceptables if (apy <= 0 || apy > 25) { - return 0; } @@ -110,12 +106,21 @@ class ApyManager extends ChangeNotifier { sortedHistory.sort((a, b) => b.timestamp.compareTo(a.timestamp)); // Périodes de temps à essayer (en minutes) - final List timePeriods = [180, 120, 60, 30, 15]; // 3h, 2h, 1h, 30min, 15min + final List timePeriods = [ + 180, + 120, + 60, + 30, + 15 + ]; // 3h, 2h, 1h, 30min, 15min // Parcourir l'historique pour trouver des paires valides for (int i = 0; i < sortedHistory.length - 1 && validPairsFound < 3; i++) { - for (int j = i + 1; j < sortedHistory.length && validPairsFound < 3; j++) { - Duration timeDiff = sortedHistory[i].timestamp.difference(sortedHistory[j].timestamp); + for (int j = i + 1; + j < sortedHistory.length && validPairsFound < 3; + j++) { + Duration timeDiff = + sortedHistory[i].timestamp.difference(sortedHistory[j].timestamp); // Vérifier si l'intervalle de temps correspond à une des périodes recherchées if (timePeriods.contains(timeDiff.inMinutes)) { @@ -125,7 +130,7 @@ class ApyManager extends ChangeNotifier { totalAPY += apy; count++; validPairsFound++; - + break; // Passer à la prochaine paire de base } } @@ -139,7 +144,6 @@ class ApyManager extends ChangeNotifier { // Vérification finale pour NaN if (result.isNaN) { - return 0; } @@ -181,8 +185,11 @@ class ApyManager extends ChangeNotifier { if (history.length < 2) return 0; // Filtrer les enregistrements pour ne considérer que ceux des derniers "maxDays" jours - final DateTime cutoffDate = DateTime.now().subtract(Duration(days: useMaxDays)); - final List recentHistory = history.where((record) => record.timestamp.isAfter(cutoffDate)).toList(); + final DateTime cutoffDate = + DateTime.now().subtract(Duration(days: useMaxDays)); + final List recentHistory = history + .where((record) => record.timestamp.isAfter(cutoffDate)) + .toList(); if (recentHistory.length < 2) return 0; @@ -213,7 +220,6 @@ class ApyManager extends ChangeNotifier { // Vérification finale pour NaN if (ema.isNaN) { - return 0; } @@ -234,8 +240,11 @@ class ApyManager extends ChangeNotifier { if (history.length < 2) return 0; // Filtrer les enregistrements pour ne considérer que ceux des derniers "maxDays" jours - final DateTime cutoffDate = DateTime.now().subtract(Duration(days: useMaxDays)); - final List recentHistory = history.where((record) => record.timestamp.isAfter(cutoffDate)).toList(); + final DateTime cutoffDate = + DateTime.now().subtract(Duration(days: useMaxDays)); + final List recentHistory = history + .where((record) => record.timestamp.isAfter(cutoffDate)) + .toList(); if (recentHistory.length < 2) return 0; @@ -252,10 +261,15 @@ class ApyManager extends ChangeNotifier { if (apy > 0 && apy <= 25 && !apy.isNaN) { // Calculer un poids en fonction de la récence (les plus récents ont plus de poids) // et de la durée entre les mesures (les mesures plus rapprochées ont plus de poids) - final double recencyWeight = math.exp(0.1 * (i - recentHistory.length + 1)); + final double recencyWeight = + math.exp(0.1 * (i - recentHistory.length + 1)); // La durée entre les mesures (en jours) - inversement proportionnelle au poids - final double durationInDays = recentHistory[i].timestamp.difference(recentHistory[i - 1].timestamp).inHours / 24; + final double durationInDays = recentHistory[i] + .timestamp + .difference(recentHistory[i - 1].timestamp) + .inHours / + 24; // Plus la durée est courte, plus le poids est élevé final double frequencyWeight = 1.0 / math.max(1.0, durationInDays); @@ -275,7 +289,6 @@ class ApyManager extends ChangeNotifier { // Vérification finale pour NaN if (result.isNaN) { - return 0; } @@ -306,7 +319,6 @@ class ApyManager extends ChangeNotifier { // Vérifier si le résultat est NaN et le remplacer par 0 le cas échéant if (result.isNaN) { - return 0.0; } @@ -383,9 +395,12 @@ class ApyManager extends ChangeNotifier { ); // Calcul des intérêts gagnés sur les dépôts - final double usdcDepositInterest = usdcDepositBalance * (usdcDepositApy / 100); - final double xdaiDepositInterest = xdaiDepositBalance * (xdaiDepositApy / 100); - final double totalDepositInterest = usdcDepositInterest + xdaiDepositInterest; + final double usdcDepositInterest = + usdcDepositBalance * (usdcDepositApy / 100); + final double xdaiDepositInterest = + xdaiDepositBalance * (xdaiDepositApy / 100); + final double totalDepositInterest = + usdcDepositInterest + xdaiDepositInterest; // Calcul des intérêts payés sur les emprunts final double usdcBorrowInterest = usdcBorrowBalance * (usdcBorrowApy / 100); @@ -441,12 +456,14 @@ class ApyManager extends ChangeNotifier { } // Calcul du ROI simple (en pourcentage) - final double simpleRoi = ((currentValue - initialInvestment) / initialInvestment) * 100; + final double simpleRoi = + ((currentValue - initialInvestment) / initialInvestment) * 100; // Calcul du ROI annualisé si la période est différente d'un an if (timeInYears != 1.0 && timeInYears > 0) { // Formule du ROI annualisé : (1 + ROI)^(1/temps) - 1 - final double annualizedRoi = (math.pow((1 + simpleRoi / 100), (1 / timeInYears)) - 1) * 100; + final double annualizedRoi = + (math.pow((1 + simpleRoi / 100), (1 / timeInYears)) - 1) * 100; return annualizedRoi; } @@ -498,5 +515,4 @@ class ApyManager extends ChangeNotifier { return Colors.green; } } - } diff --git a/lib/managers/archive_manager.dart b/lib/managers/archive_manager.dart index afa147e..74ada5f 100644 --- a/lib/managers/archive_manager.dart +++ b/lib/managers/archive_manager.dart @@ -5,7 +5,6 @@ import '../models/roi_record.dart'; import '../models/apy_record.dart'; import '../models/healthandltv_record.dart'; import '../models/rented_record.dart'; -import '../models/rented_record.dart'; import 'data_manager.dart'; import 'dart:convert'; @@ -27,17 +26,31 @@ class ArchiveManager { // 1. D'abord, lire les données existantes dans balanceHistory var boxBalance = Hive.box('balanceHistory'); - List? balanceHistoryJson = boxBalance.get('balanceHistory_totalWalletValue'); - List balanceHistory = balanceHistoryJson != null ? balanceHistoryJson.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).toList() : []; - - debugPrint("📊 Historique balanceHistory: ${balanceHistory.length} enregistrements"); + List? balanceHistoryJson = + boxBalance.get('balanceHistory_totalWalletValue'); + List balanceHistory = balanceHistoryJson != null + ? balanceHistoryJson + .map((recordJson) => + BalanceRecord.fromJson(Map.from(recordJson))) + .toList() + : []; + + debugPrint( + "📊 Historique balanceHistory: ${balanceHistory.length} enregistrements"); // 2. Ensuite, lire les données existantes dans walletValueArchive var boxWalletValue = Hive.box('walletValueArchive'); - List? walletValueArchiveJson = boxWalletValue.get('balanceHistory_totalWalletValue'); - List walletValueArchive = walletValueArchiveJson != null ? walletValueArchiveJson.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).toList() : []; - - debugPrint("📊 Historique walletValueArchive: ${walletValueArchive.length} enregistrements"); + List? walletValueArchiveJson = + boxWalletValue.get('balanceHistory_totalWalletValue'); + List walletValueArchive = walletValueArchiveJson != null + ? walletValueArchiveJson + .map((recordJson) => + BalanceRecord.fromJson(Map.from(recordJson))) + .toList() + : []; + + debugPrint( + "📊 Historique walletValueArchive: ${walletValueArchive.length} enregistrements"); // Si l'historique existe, vérifier si on doit ajouter un nouvel enregistrement if (balanceHistory.isNotEmpty) { @@ -45,7 +58,8 @@ class ArchiveManager { DateTime lastTimestamp = lastRecord.timestamp; if (DateTime.now().difference(lastTimestamp).inHours < 1) { - debugPrint("⏱️ Dernière archive trop récente (< 1h), aucun nouvel enregistrement ajouté"); + debugPrint( + "⏱️ Dernière archive trop récente (< 1h), aucun nouvel enregistrement ajouté"); return; } } @@ -65,16 +79,20 @@ class ArchiveManager { walletValueArchive = List.from(balanceHistory); // Maintenant sauvegarder les deux listes - List> balanceHistoryJsonToSave = balanceHistory.map((record) => record.toJson()).toList(); + List> balanceHistoryJsonToSave = + balanceHistory.map((record) => record.toJson()).toList(); - List> walletValueArchiveJsonToSave = walletValueArchive.map((record) => record.toJson()).toList(); + List> walletValueArchiveJsonToSave = + walletValueArchive.map((record) => record.toJson()).toList(); // Sauvegarder dans les deux boîtes Hive - await boxBalance.put('balanceHistory_totalWalletValue', balanceHistoryJsonToSave); - await boxWalletValue.put('balanceHistory_totalWalletValue', walletValueArchiveJsonToSave); - - debugPrint("✅ Archivage terminé - Nouvel enregistrement ajouté, total: ${balanceHistory.length} enregistrements"); + await boxBalance.put( + 'balanceHistory_totalWalletValue', balanceHistoryJsonToSave); + await boxWalletValue.put( + 'balanceHistory_totalWalletValue', walletValueArchiveJsonToSave); + debugPrint( + "✅ Archivage terminé - Nouvel enregistrement ajouté, total: ${balanceHistory.length} enregistrements"); } Future archiveRentedValue(double rentedValue) async { @@ -82,14 +100,20 @@ class ArchiveManager { var box = Hive.box('rentedArchive'); List? rentedHistoryJson = box.get('rented_history'); - List rentedHistory = rentedHistoryJson != null ? rentedHistoryJson.map((recordJson) => RentedRecord.fromJson(Map.from(recordJson))).toList() : []; + List rentedHistory = rentedHistoryJson != null + ? rentedHistoryJson + .map((recordJson) => + RentedRecord.fromJson(Map.from(recordJson))) + .toList() + : []; if (rentedHistory.isNotEmpty) { RentedRecord lastRecord = rentedHistory.last; DateTime lastTimestamp = lastRecord.timestamp; if (DateTime.now().difference(lastTimestamp).inHours < 1) { - debugPrint('Dernière archive récente, aucun nouvel enregistrement ajouté.'); + debugPrint( + 'Dernière archive récente, aucun nouvel enregistrement ajouté.'); return; } } @@ -100,7 +124,8 @@ class ArchiveManager { ); rentedHistory.add(newRecord); - List> rentedHistoryJsonToSave = rentedHistory.map((record) => record.toJson()).toList(); + List> rentedHistoryJsonToSave = + rentedHistory.map((record) => record.toJson()).toList(); await box.put('rented_history', rentedHistoryJsonToSave); debugPrint('Nouvel enregistrement ROI ajouté et sauvegardé avec succès.'); } catch (e) { @@ -109,34 +134,41 @@ class ArchiveManager { } Future archiveRoiValue(double roiValue) async { - debugPrint("🗃️ Début archivage ROI: valeur=${roiValue.toStringAsFixed(3)}%"); + debugPrint( + "🗃️ Début archivage ROI: valeur=${roiValue.toStringAsFixed(3)}%"); try { - debugPrint('🗃️ Début archivage ROI: valeur=${roiValue.toStringAsFixed(3)}%'); + debugPrint( + '🗃️ Début archivage ROI: valeur=${roiValue.toStringAsFixed(3)}%'); var box = Hive.box('roiValueArchive'); List? roiHistoryJson = box.get('roi_history'); - + // Vérifier si les données sont nulles et initialiser avec une liste vide si nécessaire if (roiHistoryJson == null) { - debugPrint('🗃️ ROI: Aucun historique trouvé, initialisation d\'une nouvelle liste'); + debugPrint( + '🗃️ ROI: Aucun historique trouvé, initialisation d\'une nouvelle liste'); roiHistoryJson = []; } else { - debugPrint('🗃️ ROI: Historique existant trouvé avec ${roiHistoryJson.length} entrées'); + debugPrint( + '🗃️ ROI: Historique existant trouvé avec ${roiHistoryJson.length} entrées'); } - - List roiHistory = roiHistoryJson.map((recordJson) => - ROIRecord.fromJson(Map.from(recordJson)) - ).toList(); + + List roiHistory = roiHistoryJson + .map((recordJson) => + ROIRecord.fromJson(Map.from(recordJson))) + .toList(); if (roiHistory.isNotEmpty) { ROIRecord lastRecord = roiHistory.last; DateTime lastTimestamp = lastRecord.timestamp; Duration timeSinceLastRecord = DateTime.now().difference(lastTimestamp); - debugPrint('🗃️ ROI: Dernier enregistrement du ${lastTimestamp.toIso8601String()} (il y a ${timeSinceLastRecord.inHours}h)'); - + debugPrint( + '🗃️ ROI: Dernier enregistrement du ${lastTimestamp.toIso8601String()} (il y a ${timeSinceLastRecord.inHours}h)'); + if (timeSinceLastRecord.inHours < 1) { - debugPrint('🗃️ ROI: Dernière archive trop récente (<1h), aucun nouvel enregistrement ajouté'); + debugPrint( + '🗃️ ROI: Dernière archive trop récente (<1h), aucun nouvel enregistrement ajouté'); return; } } else { @@ -149,18 +181,21 @@ class ArchiveManager { ); roiHistory.add(newRecord); - List> roiHistoryJsonToSave = roiHistory.map((record) => record.toJson()).toList(); + List> roiHistoryJsonToSave = + roiHistory.map((record) => record.toJson()).toList(); await box.put('roi_history', roiHistoryJsonToSave); - debugPrint('🗃️ ROI: Nouvel enregistrement ajouté avec succès, total: ${roiHistory.length} enregistrements'); - + debugPrint( + '🗃️ ROI: Nouvel enregistrement ajouté avec succès, total: ${roiHistory.length} enregistrements'); + // Afficher quelques enregistrements pour le débogage - if (roiHistory.length > 0) { - debugPrint('🗃️ ROI: Dernier enregistrement: ${roiHistory.last.roi}% (${roiHistory.last.timestamp.toIso8601String()})'); + if (roiHistory.isNotEmpty) { + debugPrint( + '🗃️ ROI: Dernier enregistrement: ${roiHistory.last.roi}% (${roiHistory.last.timestamp.toIso8601String()})'); } if (roiHistory.length > 1) { - debugPrint('🗃️ ROI: Avant-dernier enregistrement: ${roiHistory[roiHistory.length-2].roi}% (${roiHistory[roiHistory.length-2].timestamp.toIso8601String()})'); + debugPrint( + '🗃️ ROI: Avant-dernier enregistrement: ${roiHistory[roiHistory.length - 2].roi}% (${roiHistory[roiHistory.length - 2].timestamp.toIso8601String()})'); } - } catch (e) { debugPrint('❌ Erreur lors de l\'archivage de la valeur ROI : $e'); } @@ -173,7 +208,7 @@ class ArchiveManager { List? apyHistoryJson = box.get('apy_history'); List apyHistory = []; - + // Chargement robuste des données existantes if (apyHistoryJson != null) { for (var recordJson in apyHistoryJson) { @@ -185,7 +220,8 @@ class ArchiveManager { } else if (recordJson is Map) { recordMap = Map.from(recordJson); } else { - debugPrint("⚠️ Enregistrement APY ignoré (format invalide): $recordJson"); + debugPrint( + "⚠️ Enregistrement APY ignoré (format invalide): $recordJson"); continue; } @@ -193,7 +229,8 @@ class ArchiveManager { final apyRecord = APYRecord.fromJson(recordMap); apyHistory.add(apyRecord); } catch (e) { - debugPrint("⚠️ Erreur lors du décodage d'un enregistrement APY: $e"); + debugPrint( + "⚠️ Erreur lors du décodage d'un enregistrement APY: $e"); debugPrint("📄 Données problématiques: $recordJson"); continue; // Ignorer cet enregistrement et continuer } @@ -215,14 +252,18 @@ class ArchiveManager { apyHistory.sort((a, b) => a.timestamp.compareTo(b.timestamp)); // Convertir en JSON avec gestion sécurisée - final apyHistoryJsonList = apyHistory.map((record) { - try { - return record.toJson(); - } catch (e) { - debugPrint("⚠️ Erreur lors de la conversion JSON d'un enregistrement APY: $e"); - return null; - } - }).where((json) => json != null).toList(); + final apyHistoryJsonList = apyHistory + .map((record) { + try { + return record.toJson(); + } catch (e) { + debugPrint( + "⚠️ Erreur lors de la conversion JSON d'un enregistrement APY: $e"); + return null; + } + }) + .where((json) => json != null) + .toList(); // Sauvegarder dans la boîte Hive await box.put('apy_history', apyHistoryJsonList); @@ -230,16 +271,18 @@ class ArchiveManager { // Marquer la dernière mise à jour await box.put('lastApyArchiveTime', now.toIso8601String()); - debugPrint("✅ Valeur APY archivée avec succès: Net $netApyValue%, Gross $grossApyValue%"); - debugPrint("📊 Total des enregistrements APY: ${apyHistoryJsonList.length}"); - + debugPrint( + "✅ Valeur APY archivée avec succès: Net $netApyValue%, Gross $grossApyValue%"); + debugPrint( + "📊 Total des enregistrements APY: ${apyHistoryJsonList.length}"); } catch (e) { debugPrint("❌ Erreur lors de l'archivage des valeurs APY : $e"); - throw e; // Relancer l'erreur pour que le code appelant puisse la gérer + rethrow; // Relancer l'erreur pour que le code appelant puisse la gérer } } - Future archiveBalance(String tokenType, double balance, String timestamp) async { + Future archiveBalance( + String tokenType, double balance, String timestamp) async { try { var box = Hive.box('balanceHistory'); @@ -248,14 +291,21 @@ class ArchiveManager { if (rawData != null) { if (rawData is List) { - balanceHistory = rawData.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).toList(); + balanceHistory = rawData + .map((recordJson) => + BalanceRecord.fromJson(Map.from(recordJson))) + .toList(); } else if (rawData is String) { // Si les données sont une chaîne JSON, on essaie de les parser try { List parsedData = json.decode(rawData); - balanceHistory = parsedData.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).toList(); + balanceHistory = parsedData + .map((recordJson) => BalanceRecord.fromJson( + Map.from(recordJson))) + .toList(); } catch (e) { - debugPrint("⚠️ Erreur lors du parsing des données JSON pour $tokenType: $e"); + debugPrint( + "⚠️ Erreur lors du parsing des données JSON pour $tokenType: $e"); } } } @@ -265,7 +315,8 @@ class ArchiveManager { try { parsedTimestamp = DateTime.parse(timestamp); } catch (e) { - debugPrint("⚠️ Format de timestamp invalide '$timestamp', utilisation de l'heure actuelle: $e"); + debugPrint( + "⚠️ Format de timestamp invalide '$timestamp', utilisation de l'heure actuelle: $e"); parsedTimestamp = DateTime.now(); } @@ -277,30 +328,40 @@ class ArchiveManager { balanceHistory.add(newRecord); - List> balanceHistoryJsonToSave = balanceHistory.map((record) => record.toJson()).toList(); + List> balanceHistoryJsonToSave = + balanceHistory.map((record) => record.toJson()).toList(); await box.put('balanceHistory_$tokenType', balanceHistoryJsonToSave); - debugPrint("📊 Archivage de la balance - Token: $tokenType, Balance: ${balance.toStringAsFixed(3)}, Timestamp: ${parsedTimestamp.toIso8601String()}"); + debugPrint( + "📊 Archivage de la balance - Token: $tokenType, Balance: ${balance.toStringAsFixed(3)}, Timestamp: ${parsedTimestamp.toIso8601String()}"); } catch (e) { - debugPrint('❌ Erreur lors de l\'archivage de la balance pour $tokenType : $e'); + debugPrint( + '❌ Erreur lors de l\'archivage de la balance pour $tokenType : $e'); debugPrint('Stack trace: ${StackTrace.current}'); } } - Future archiveHealthAndLtvValue(double healtFactorValue, double ltvValue) async { + Future archiveHealthAndLtvValue( + double healtFactorValue, double ltvValue) async { try { var box = Hive.box('HealthAndLtvValueArchive'); List? healthAndLtvHistoryJson = box.get('healthAndLtv_history'); List healthAndLtvHistory = - healthAndLtvHistoryJson != null ? healthAndLtvHistoryJson.map((recordJson) => HealthAndLtvRecord.fromJson(Map.from(recordJson))).toList() : []; + healthAndLtvHistoryJson != null + ? healthAndLtvHistoryJson + .map((recordJson) => HealthAndLtvRecord.fromJson( + Map.from(recordJson))) + .toList() + : []; if (healthAndLtvHistory.isNotEmpty) { HealthAndLtvRecord lastRecord = healthAndLtvHistory.last; DateTime lastTimestamp = lastRecord.timestamp; if (DateTime.now().difference(lastTimestamp).inHours < 1) { - debugPrint('Dernier enregistrement récent, aucun nouvel enregistrement ajouté.'); + debugPrint( + 'Dernier enregistrement récent, aucun nouvel enregistrement ajouté.'); return; } } @@ -312,7 +373,8 @@ class ArchiveManager { ); healthAndLtvHistory.add(newRecord); - List> healthAndLtvHistoryJsonToSave = healthAndLtvHistory.map((record) => record.toJson()).toList(); + List> healthAndLtvHistoryJsonToSave = + healthAndLtvHistory.map((record) => record.toJson()).toList(); await box.put('healthAndLtv_history', healthAndLtvHistoryJsonToSave); debugPrint('Nouvel enregistrement APY ajouté et sauvegardé avec succès.'); @@ -325,7 +387,11 @@ class ArchiveManager { var box = Hive.box('balanceHistory'); List? balanceHistoryJson = box.get('balanceHistory_$tokenType'); - return balanceHistoryJson!.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).where((record) => record.tokenType == tokenType).toList(); + return balanceHistoryJson! + .map((recordJson) => + BalanceRecord.fromJson(Map.from(recordJson))) + .where((record) => record.tokenType == tokenType) + .toList(); } /// Archive la valeur de loyer actuelle @@ -335,25 +401,27 @@ class ArchiveManager { await Hive.openBox('rentData'); } var box = Hive.box('rentData'); - + final timestamp = DateTime.now(); - + // Créer un nouvel enregistrement RentRecord final rentRecord = RentedRecord( percentage: percentage, timestamp: timestamp, ); - + // Récupérer les données existantes List existingData = box.get('rentHistory', defaultValue: []); - List> rentHistory = List>.from(existingData); - + List> rentHistory = + List>.from(existingData); + // Calculer le temps écoulé depuis le dernier enregistrement bool shouldArchive = rentHistory.isEmpty; if (!shouldArchive && rentHistory.isNotEmpty) { final lastRecord = RentedRecord.fromJson(rentHistory.last); - final daysSinceLastRecord = timestamp.difference(lastRecord.timestamp).inDays; - + final daysSinceLastRecord = + timestamp.difference(lastRecord.timestamp).inDays; + // Déterminer si l'archivage doit avoir lieu en fonction du nombre d'enregistrements if (rentHistory.length < 30) { // Moins de 30 enregistrements, archiver quotidiennement @@ -369,10 +437,10 @@ class ArchiveManager { shouldArchive = daysSinceLastRecord >= 15; } } - + // Archiver la valeur si nécessaire if (shouldArchive) { - // debugPrint("📊 Archivage de la valeur de loyer: $rent, cumulatif: $cumulativeRent"); + // debugPrint("📊 Archivage de la valeur de loyer: $rent, cumulatif: $cumulativeRent"); rentHistory.add(rentRecord.toJson()); await box.put('rentHistory', rentHistory); } diff --git a/lib/managers/data_manager.dart b/lib/managers/data_manager.dart index f10951d..58b41a8 100644 --- a/lib/managers/data_manager.dart +++ b/lib/managers/data_manager.dart @@ -1,8 +1,6 @@ import 'dart:convert'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/models/healthandltv_record.dart'; import 'package:realtoken_asset_tracker/models/rented_record.dart'; import 'package:realtoken_asset_tracker/utils/parameters.dart'; @@ -37,9 +35,9 @@ class DataManager extends ChangeNotifier { archiveManager: ArchiveManager(), apyManager: ApyManager(), ); - + factory DataManager() => _instance; - + // Variables finales pour les managers final ArchiveManager _archiveManager; final ApyManager apyManager; @@ -54,12 +52,12 @@ class DataManager extends ChangeNotifier { DataManager._internal({ required ArchiveManager archiveManager, required ApyManager apyManager, - }) : _archiveManager = archiveManager, - apyManager = apyManager { + }) : _archiveManager = archiveManager, + apyManager = apyManager { _initializeServices(); // Initialiser les services loadCustomInitPrices(); // Charger les prix personnalisés lors de l'initialisation _loadApyReactivityPreference(); // Charger la préférence de réactivité APY - + // Initialiser l'ArchiveManager avec une référence à cette instance _archiveManager.setDataManager(this); } @@ -78,18 +76,20 @@ class DataManager extends ChangeNotifier { Future _loadApyReactivityPreference() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de la préférence de réactivité APY..."); - + try { final prefs = await SharedPreferences.getInstance(); double reactivity = prefs.getDouble('apyReactivity') ?? 0.2; - + // Appliquer la valeur de réactivité aux paramètres de l'ApyManager adjustApyReactivity(reactivity); - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Préférence de réactivité APY chargée: $reactivity (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Préférence de réactivité APY chargée: $reactivity (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de la préférence de réactivité APY: $e"); + debugPrint( + "$_logError Erreur lors du chargement de la préférence de réactivité APY: $e"); } } @@ -97,7 +97,8 @@ class DataManager extends ChangeNotifier { bool isLoadingSecondary = true; bool isLoadingMain = true; bool isLoadingTransactions = true; - bool isUpdatingData = false; // Nouvelle variable pour suivre les mises à jour en cours + bool isUpdatingData = + false; // Nouvelle variable pour suivre les mises à jour en cours // Variables globales pour la gestion des données Map tokenDataFetched = {}; @@ -110,9 +111,12 @@ class DataManager extends ChangeNotifier { List> detailedRentData = []; // Structures de données pour les loyers - Map cumulativeRentsByToken = {}; // Pour tous les wallets combinés - Map> cumulativeRentsByWallet = {}; // Par wallet puis par token - Map tokensWalletCount = {}; // Nombre de wallets possédant chaque token + Map cumulativeRentsByToken = + {}; // Pour tous les wallets combinés + Map> cumulativeRentsByWallet = + {}; // Par wallet puis par token + Map tokensWalletCount = + {}; // Nombre de wallets possédant chaque token List> rentHistory = []; // Nouvelle structure de données pour les statistiques détaillées des wallets @@ -169,7 +173,7 @@ class DataManager extends ChangeNotifier { List> rentData = []; List> propertyData = []; List> perWalletBalances = []; - + List> _allTokens = []; // Liste privée pour tous les tokens List> get allTokens => _allTokens; @@ -194,7 +198,8 @@ class DataManager extends ChangeNotifier { List> yamHistory = []; Map>> transactionsByToken = {}; List> whitelistTokens = []; - List> tokenHistoryData = []; // Historique des modifications des tokens + List> tokenHistoryData = + []; // Historique des modifications des tokens var customInitPricesBox = Hive.box('CustomInitPrices'); @@ -204,46 +209,50 @@ class DataManager extends ChangeNotifier { Duration(minutes: 5); // Délai minimal avant la prochaine mise à jour // Remplacer les propriétés APY du DataManager par une instance de ApyManager - + // Supprimer les propriétés suivantes du DataManager car elles sont maintenant dans ApyManager : // depositApyUsdc, depositApyXdai, borrowApyUsdc, borrowApyXdai, initialInvestment - + // ... existing code ... Future loadWalletsAddresses({bool forceFetch = false}) async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement des adresses de wallets..."); - + final prefs = await SharedPreferences.getInstance(); // Charger les adresses evmAddresses = prefs.getStringList('evmAddresses') ?? []; - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess ${evmAddresses.length} adresses de wallets chargées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess ${evmAddresses.length} adresses de wallets chargées (${duration.inMilliseconds}ms)"); } Future updateMainInformations({bool forceFetch = false}) async { // Vérifier si déjà en cours d'exécution if (_isUpdatingMainInformations) { - debugPrint("$_logWarning updateMainInformations déjà en cours d'exécution, requête ignorée"); + debugPrint( + "$_logWarning updateMainInformations déjà en cours d'exécution, requête ignorée"); return; } - + // Vérifier si des adresses de wallet sont disponibles if (evmAddresses.isEmpty) { - debugPrint("$_logWarning updateMainInformations : aucune adresse de wallet disponible"); + debugPrint( + "$_logWarning updateMainInformations : aucune adresse de wallet disponible"); return; } - + // Marquer comme en cours d'exécution et activer les shimmers _isUpdatingMainInformations = true; isUpdatingData = true; // Active les shimmers dans l'UI notifyListeners(); // Notifier les observateurs pour afficher les shimmers - + final startTime = DateTime.now(); var box = Hive.box('realTokens'); // Ouvrir la boîte Hive pour le cache - debugPrint("$_logMain Début de la mise à jour des informations principales..."); + debugPrint( + "$_logMain Début de la mise à jour des informations principales..."); // Vérifier si une mise à jour est nécessaire if (!forceFetch && @@ -272,11 +281,13 @@ class DataManager extends ChangeNotifier { var data = await apiCall(); if (data.isNotEmpty) { final fetchDuration = DateTime.now().difference(fetchStartTime); - debugPrint("$_logSuccess Données $debugName mises à jour (${fetchDuration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Données $debugName mises à jour (${fetchDuration.inMilliseconds}ms)"); box.put(cacheKey, json.encode(data)); updateVariable(List>.from(data)); } else { - debugPrint("$_logWarning Pas de nouvelles données $debugName, chargement du cache"); + debugPrint( + "$_logWarning Pas de nouvelles données $debugName, chargement du cache"); var cachedData = box.get(cacheKey); if (cachedData != null) { updateVariable( @@ -338,20 +349,22 @@ class DataManager extends ChangeNotifier { // Charger les historiques debugPrint("$_logSub Chargement des historiques..."); final histStartTime = DateTime.now(); - + loadWalletBalanceHistory(); loadRentedHistory(); loadRoiHistory(); loadApyHistory(); loadHealthAndLtvHistory(); - + final histDuration = DateTime.now().difference(histStartTime); - debugPrint("$_logSuccess Historiques chargés (${histDuration.inMilliseconds}ms)"); - + debugPrint( + "$_logSuccess Historiques chargés (${histDuration.inMilliseconds}ms)"); + isLoadingMain = false; - + final totalDuration = DateTime.now().difference(startTime); - debugPrint("$_logMain Mise à jour principale terminée (${totalDuration.inMilliseconds}ms)"); + debugPrint( + "$_logMain Mise à jour principale terminée (${totalDuration.inMilliseconds}ms)"); } catch (e) { debugPrint("$_logError Erreur globale dans updateMainInformations: $e"); } finally { @@ -365,23 +378,26 @@ class DataManager extends ChangeNotifier { {bool forceFetch = false}) async { // Vérifier si déjà en cours d'exécution if (_isUpdatingSecondaryInformations) { - debugPrint("$_logWarning updateSecondaryInformations déjà en cours d'exécution, requête ignorée"); + debugPrint( + "$_logWarning updateSecondaryInformations déjà en cours d'exécution, requête ignorée"); return; } - + // Vérifier si des adresses de wallet sont disponibles if (evmAddresses.isEmpty) { - debugPrint("$_logWarning updateSecondaryInformations : aucune adresse de wallet disponible"); + debugPrint( + "$_logWarning updateSecondaryInformations : aucune adresse de wallet disponible"); return; } - + // Marquer comme en cours d'exécution _isUpdatingSecondaryInformations = true; - + final startTime = DateTime.now(); var box = Hive.box('realTokens'); // Ouvrir la boîte Hive pour le cache - - debugPrint("$_logMain Début de la mise à jour des informations secondaires..."); + + debugPrint( + "$_logMain Début de la mise à jour des informations secondaires..."); try { // Fonction générique pour fetch + cache @@ -397,11 +413,13 @@ class DataManager extends ChangeNotifier { var data = await apiCall(); if (data.isNotEmpty) { final fetchDuration = DateTime.now().difference(fetchStartTime); - debugPrint("$_logSuccess Données $debugName mises à jour (${fetchDuration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Données $debugName mises à jour (${fetchDuration.inMilliseconds}ms)"); box.put(cacheKey, json.encode(data)); updateVariable(List>.from(data)); } else { - debugPrint("$_logWarning Pas de nouvelles données $debugName, chargement du cache"); + debugPrint( + "$_logWarning Pas de nouvelles données $debugName, chargement du cache"); var cachedData = box.get(cacheKey); if (cachedData != null) { updateVariable( @@ -436,34 +454,38 @@ class DataManager extends ChangeNotifier { }, debugName: "YAM Volumes History"), fetchData( - apiCall: () => ApiService.fetchTransactionsHistory(forceFetch: forceFetch), - cacheKey: 'transactionsHistory', - updateVariable: (data) async { - transactionsHistory = data; - await processTransactionsHistory(context, transactionsHistory, yamWalletsTransactionsFetched); - }, - debugName: "Transactions History" - ), + apiCall: () => + ApiService.fetchTransactionsHistory(forceFetch: forceFetch), + cacheKey: 'transactionsHistory', + updateVariable: (data) async { + transactionsHistory = data; + await processTransactionsHistory( + context, transactionsHistory, yamWalletsTransactionsFetched); + }, + debugName: "Transactions History"), fetchData( - apiCall: () => ApiService.fetchDetailedRentDataForAllWallets(forceFetch: forceFetch), - cacheKey: 'detailedRentData', - updateVariable: (data) { - detailedRentData = data; - // Traiter les données détaillées de loyer immédiatement après les avoir récupérées - processDetailedRentData(); - }, - debugName: "Detailed rents" - ), + apiCall: () => ApiService.fetchDetailedRentDataForAllWallets( + forceFetch: forceFetch), + cacheKey: 'detailedRentData', + updateVariable: (data) { + detailedRentData = data; + // Traiter les données détaillées de loyer immédiatement après les avoir récupérées + processDetailedRentData(); + }, + debugName: "Detailed rents"), ]); isLoadingSecondary = false; - + final totalDuration = DateTime.now().difference(startTime); - debugPrint("$_logMain Mise à jour secondaire terminée (${totalDuration.inMilliseconds}ms)"); + debugPrint( + "$_logMain Mise à jour secondaire terminée (${totalDuration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur globale dans updateSecondaryInformations: $e"); + debugPrint( + "$_logError Erreur globale dans updateSecondaryInformations: $e"); } finally { - _isUpdatingSecondaryInformations = false; // Réinitialiser le flag quoi qu'il arrive + _isUpdatingSecondaryInformations = + false; // Réinitialiser le flag quoi qu'il arrive } } @@ -471,66 +493,69 @@ class DataManager extends ChangeNotifier { void processDetailedRentData() { final startTime = DateTime.now(); debugPrint("$_logSub Traitement des données détaillées de loyer..."); - + // Réinitialiser les structures de données cumulativeRentsByToken = {}; cumulativeRentsByWallet = {}; tokensWalletCount = {}; rentHistory = []; - + // Si aucune donnée détaillée, sortir if (detailedRentData.isEmpty) { debugPrint("$_logWarning Aucune donnée détaillée de loyer disponible"); return; } - + try { // Parcourir chaque entrée de date for (var dateEntry in detailedRentData) { // Vérifier les champs obligatoires date et rents if (!dateEntry.containsKey('date') || !dateEntry.containsKey('rents')) { - debugPrint("$_logWarning Format de données incorrect pour une entrée"); + debugPrint( + "$_logWarning Format de données incorrect pour une entrée"); continue; } - + String date = dateEntry['date']; List rents = dateEntry['rents']; - + // Récupérer le wallet s'il existe, sinon utiliser "unknown" String wallet = "unknown"; if (dateEntry.containsKey('wallet') && dateEntry['wallet'] != null) { wallet = dateEntry['wallet'].toLowerCase(); } - + // Initialiser le map pour ce wallet s'il n'existe pas if (!cumulativeRentsByWallet.containsKey(wallet)) { cumulativeRentsByWallet[wallet] = {}; } - + // Ajouter l'entrée à l'historique rentHistory.add({ 'date': date, 'wallet': wallet, 'rents': List>.from(rents) }); - + // Parcourir chaque loyer pour cette date for (var rentEntry in rents) { String token = rentEntry['token'].toLowerCase(); - double rent = (rentEntry['rent'] is num) - ? (rentEntry['rent'] as num).toDouble() - : double.tryParse(rentEntry['rent'].toString()) ?? 0.0; - + double rent = (rentEntry['rent'] is num) + ? (rentEntry['rent'] as num).toDouble() + : double.tryParse(rentEntry['rent'].toString()) ?? 0.0; + // Additionner au total cumulé pour ce token (tous wallets confondus) - cumulativeRentsByToken[token] = (cumulativeRentsByToken[token] ?? 0.0) + rent; - + cumulativeRentsByToken[token] = + (cumulativeRentsByToken[token] ?? 0.0) + rent; + // Additionner au total cumulé pour ce token dans ce wallet - cumulativeRentsByWallet[wallet]![token] = (cumulativeRentsByWallet[wallet]![token] ?? 0.0) + rent; - + cumulativeRentsByWallet[wallet]![token] = + (cumulativeRentsByWallet[wallet]![token] ?? 0.0) + rent; + // Compter les wallets uniques pour chaque token Set walletsForToken = {}; for (var walletKey in cumulativeRentsByWallet.keys) { - if (cumulativeRentsByWallet[walletKey]!.containsKey(token) && + if (cumulativeRentsByWallet[walletKey]!.containsKey(token) && cumulativeRentsByWallet[walletKey]![token]! > 0) { walletsForToken.add(walletKey); } @@ -538,11 +563,13 @@ class DataManager extends ChangeNotifier { tokensWalletCount[token] = walletsForToken.length; } } - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Traitement terminé: ${cumulativeRentsByToken.length} tokens, ${cumulativeRentsByWallet.length} wallets, ${rentHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Traitement terminé: ${cumulativeRentsByToken.length} tokens, ${cumulativeRentsByWallet.length} wallets, ${rentHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du traitement des données détaillées de loyer: $e"); + debugPrint( + "$_logError Erreur lors du traitement des données détaillées de loyer: $e"); } } @@ -552,9 +579,10 @@ class DataManager extends ChangeNotifier { if (cumulativeRentsByToken.containsKey(token.toLowerCase())) { return cumulativeRentsByToken[token.toLowerCase()]!; } - + // Si la valeur n'est pas précalculée (fallback), on calcule à la demande - debugPrint("$_logWarning Calcul des loyers à la demande pour le token: $token (non trouvé dans les données précalculées)"); + debugPrint( + "$_logWarning Calcul des loyers à la demande pour le token: $token (non trouvé dans les données précalculées)"); double totalRent = 0.0; // Parcourir chaque entrée de la liste detailedRentData @@ -579,28 +607,29 @@ class DataManager extends ChangeNotifier { Future loadFromCacheThenUpdate(BuildContext context) async { // Vérifier si déjà en cours d'exécution if (_isLoadingFromCache) { - debugPrint("$_logWarning loadFromCacheThenUpdate déjà en cours d'exécution, requête ignorée"); + debugPrint( + "$_logWarning loadFromCacheThenUpdate déjà en cours d'exécution, requête ignorée"); return; } // Marquer comme en cours d'exécution _isLoadingFromCache = true; - + final startTime = DateTime.now(); debugPrint("$_logMain Chargement optimisé cache-first..."); - + try { var box = Hive.box('realTokens'); - + // Charger les adresses de wallet await loadWalletsAddresses(); - + if (evmAddresses.isEmpty) { debugPrint("$_logWarning Aucune adresse de wallet disponible"); _isLoadingFromCache = false; return; } - + // Fonction générique de chargement depuis le cache avec gestion d'erreur Future loadFromCacheWithFallback({ required String cacheKey, @@ -611,17 +640,19 @@ class DataManager extends ChangeNotifier { try { // Essayer avec la clé principale var cachedData = box.get(cacheKey); - + // Si pas de données, essayer avec la clé alternative if (cachedData == null && alternativeCacheKey != null) { cachedData = box.get(alternativeCacheKey); } - + if (cachedData != null) { try { - final decodedData = List>.from(json.decode(cachedData)); + final decodedData = + List>.from(json.decode(cachedData)); updateVariable(decodedData); - debugPrint("$_logSuccess Cache $debugName chargé: ${decodedData.length} éléments"); + debugPrint( + "$_logSuccess Cache $debugName chargé: ${decodedData.length} éléments"); } catch (e) { debugPrint("$_logError Erreur décodage cache $debugName: $e"); } @@ -632,29 +663,26 @@ class DataManager extends ChangeNotifier { debugPrint("$_logError Erreur chargement cache $debugName: $e"); } } - + // 1. Chargement prioritaire en parallèle des données principales debugPrint("$_logSub Chargement prioritaire du cache principal..."); await Future.wait([ loadFromCacheWithFallback( - cacheKey: 'cachedData_wallet_tokens', - alternativeCacheKey: 'cachedTokenData_tokens', - updateVariable: (data) => walletTokens = data, - debugName: "Tokens" - ), + cacheKey: 'cachedData_wallet_tokens', + alternativeCacheKey: 'cachedTokenData_tokens', + updateVariable: (data) => walletTokens = data, + debugName: "Tokens"), loadFromCacheWithFallback( - cacheKey: 'cachedRealTokens', - updateVariable: (data) => realTokens = data, - debugName: "RealTokens" - ), + cacheKey: 'cachedRealTokens', + updateVariable: (data) => realTokens = data, + debugName: "RealTokens"), loadFromCacheWithFallback( - cacheKey: 'rmmBalances', - updateVariable: (data) { - rmmBalances = data; - if (data.isNotEmpty) fetchRmmBalances(); - }, - debugName: "RMM Balances" - ), + cacheKey: 'rmmBalances', + updateVariable: (data) { + rmmBalances = data; + if (data.isNotEmpty) fetchRmmBalances(); + }, + debugName: "RMM Balances"), ]); // Calculer les données essentielles immédiatement après le chargement du cache principal @@ -668,80 +696,71 @@ class DataManager extends ChangeNotifier { debugPrint("$_logSub Chargement du cache secondaire..."); await Future.wait([ loadFromCacheWithFallback( - cacheKey: 'cachedRentData', - updateVariable: (data) => rentData = data, - debugName: "Données de loyer" - ), + cacheKey: 'cachedRentData', + updateVariable: (data) => rentData = data, + debugName: "Données de loyer"), loadFromCacheWithFallback( - cacheKey: 'cachedData_tempRentData', - alternativeCacheKey: 'tempRentData', - updateVariable: (data) => tempRentData = data, - debugName: "Loyer temporaire" - ), + cacheKey: 'cachedData_tempRentData', + alternativeCacheKey: 'tempRentData', + updateVariable: (data) => tempRentData = data, + debugName: "Loyer temporaire"), loadFromCacheWithFallback( - cacheKey: 'cachedData_cachedPropertiesForSaleData', - alternativeCacheKey: 'cachedPropertiesForSaleData', - updateVariable: (data) => propertiesForSaleFetched = data, - debugName: "Propriétés en vente" - ), + cacheKey: 'cachedData_cachedPropertiesForSaleData', + alternativeCacheKey: 'cachedPropertiesForSaleData', + updateVariable: (data) => propertiesForSaleFetched = data, + debugName: "Propriétés en vente"), loadFromCacheWithFallback( - cacheKey: 'cachedData_cachedWhitelistTokens', - alternativeCacheKey: 'cachedWhitelistTokens', - updateVariable: (data) => whitelistTokens = data, - debugName: "Whitelist" - ), + cacheKey: 'cachedData_cachedWhitelistTokens', + alternativeCacheKey: 'cachedWhitelistTokens', + updateVariable: (data) => whitelistTokens = data, + debugName: "Whitelist"), loadFromCacheWithFallback( - cacheKey: 'cachedData_cachedTokenHistoryData', - alternativeCacheKey: 'cachedTokenHistoryData', - updateVariable: (data) { - tokenHistoryData = data; - if (data.isNotEmpty) processTokenHistory(); - }, - debugName: "Token History" - ), + cacheKey: 'cachedData_cachedTokenHistoryData', + alternativeCacheKey: 'cachedTokenHistoryData', + updateVariable: (data) { + tokenHistoryData = data; + if (data.isNotEmpty) processTokenHistory(); + }, + debugName: "Token History"), loadFromCacheWithFallback( - cacheKey: 'cachedData_cachedWalletsTransactions', - alternativeCacheKey: 'cachedWalletsTransactions', - updateVariable: (data) => yamWalletsTransactionsFetched = data, - debugName: "YAM Wallets Transactions" - ), + cacheKey: 'cachedData_cachedWalletsTransactions', + alternativeCacheKey: 'cachedWalletsTransactions', + updateVariable: (data) => yamWalletsTransactionsFetched = data, + debugName: "YAM Wallets Transactions"), loadFromCacheWithFallback( - cacheKey: 'cachedData_cachedYamMarket', - alternativeCacheKey: 'cachedYamMarket', - updateVariable: (data) => yamMarketFetched = data, - debugName: "YAM Market" - ), + cacheKey: 'cachedData_cachedYamMarket', + alternativeCacheKey: 'cachedYamMarket', + updateVariable: (data) => yamMarketFetched = data, + debugName: "YAM Market"), loadFromCacheWithFallback( - cacheKey: 'cachedData_yamHistory', - alternativeCacheKey: 'yamHistory', - updateVariable: (data) { - yamHistory = data; - if (data.isNotEmpty) fetchYamHistory(); - }, - debugName: "YAM Volumes History" - ), + cacheKey: 'cachedData_yamHistory', + alternativeCacheKey: 'yamHistory', + updateVariable: (data) { + yamHistory = data; + if (data.isNotEmpty) fetchYamHistory(); + }, + debugName: "YAM Volumes History"), loadFromCacheWithFallback( - cacheKey: 'cachedData_transactionsHistory', - alternativeCacheKey: 'transactionsHistory', - updateVariable: (data) async { - transactionsHistory = data; - if (data.isNotEmpty && yamWalletsTransactionsFetched.isNotEmpty) { - await processTransactionsHistory(context, transactionsHistory, yamWalletsTransactionsFetched); - } - }, - debugName: "Transactions History" - ), + cacheKey: 'cachedData_transactionsHistory', + alternativeCacheKey: 'transactionsHistory', + updateVariable: (data) async { + transactionsHistory = data; + if (data.isNotEmpty && yamWalletsTransactionsFetched.isNotEmpty) { + await processTransactionsHistory(context, transactionsHistory, + yamWalletsTransactionsFetched); + } + }, + debugName: "Transactions History"), loadFromCacheWithFallback( - cacheKey: 'cachedData_detailedRentData', - alternativeCacheKey: 'detailedRentData', - updateVariable: (data) { - detailedRentData = data; - if (data.isNotEmpty) processDetailedRentData(); - }, - debugName: "Detailed rents" - ) + cacheKey: 'cachedData_detailedRentData', + alternativeCacheKey: 'detailedRentData', + updateVariable: (data) { + detailedRentData = data; + if (data.isNotEmpty) processDetailedRentData(); + }, + debugName: "Detailed rents") ]); - + // 3. Charger les historiques persistants debugPrint("$_logSub Chargement des historiques..."); await Future.wait([ @@ -751,7 +770,7 @@ class DataManager extends ChangeNotifier { loadApyHistory(), loadHealthAndLtvHistory(), ]); - + // Traitement final des données secondaires if (propertiesForSaleFetched.isNotEmpty) { await fetchAndStorePropertiesForSale(); @@ -759,23 +778,23 @@ class DataManager extends ChangeNotifier { if (yamMarketFetched.isNotEmpty) { await fetchAndStoreYamMarketData(); } - + // Marquer le chargement initial comme terminé isLoadingMain = false; isLoadingSecondary = false; isLoading = false; isLoadingTransactions = false; - + final cacheDuration = DateTime.now().difference(startTime); - debugPrint("$_logMain Cache chargé et données calculées (${cacheDuration.inMilliseconds}ms)"); - + debugPrint( + "$_logMain Cache chargé et données calculées (${cacheDuration.inMilliseconds}ms)"); + // Notifier que les données du cache sont prêtes notifyListeners(); - + // 4. Lancer les mises à jour API en arrière-plan (sans bloquer l'UI) debugPrint("$_logMain Démarrage des mises à jour en arrière-plan..."); _startBackgroundUpdate(context); - } catch (e) { debugPrint("$_logError Erreur globale dans loadFromCacheThenUpdate: $e"); // En cas d'erreur, s'assurer que l'UI n'est pas bloquée @@ -795,13 +814,13 @@ class DataManager extends ChangeNotifier { // Activer les indicateurs de mise à jour en arrière-plan isUpdatingData = true; notifyListeners(); - + // Lancer les mises à jour en parallèle await Future.wait([ updateMainInformations(forceFetch: false), updateSecondaryInformations(context, forceFetch: false), ]); - + // Recalculer les données avec les nouvelles informations if (realTokens.isNotEmpty && walletTokens.isNotEmpty) { await fetchAndCalculateData(); @@ -813,10 +832,11 @@ class DataManager extends ChangeNotifier { processTokenHistory(); } } - + debugPrint("$_logSuccess Mise à jour en arrière-plan terminée"); } catch (e) { - debugPrint("$_logError Erreur lors de la mise à jour en arrière-plan: $e"); + debugPrint( + "$_logError Erreur lors de la mise à jour en arrière-plan: $e"); } finally { // Désactiver les indicateurs de mise à jour isUpdatingData = false; @@ -824,19 +844,21 @@ class DataManager extends ChangeNotifier { } }); } - + /// Méthode centralisée pour mettre à jour toutes les données /// Cette méthode coordonne toutes les mises à jour et évite la duplication de code - Future updateAllData(BuildContext context, {bool forceFetch = false}) async { + Future updateAllData(BuildContext context, + {bool forceFetch = false}) async { if (evmAddresses.isEmpty) { - debugPrint("$_logWarning updateAllData : aucune adresse de wallet disponible"); + debugPrint( + "$_logWarning updateAllData : aucune adresse de wallet disponible"); return; } - + // Mettre à jour les informations principales et secondaires await updateMainInformations(forceFetch: forceFetch); await updateSecondaryInformations(context, forceFetch: forceFetch); - + // Mettre à jour les autres données await fetchRentData(forceFetch: forceFetch); await fetchAndCalculateData(forceFetch: forceFetch); @@ -845,17 +867,18 @@ class DataManager extends ChangeNotifier { await fetchAndStoreYamMarketData(); await fetchAndStorePropertiesForSale(); } - + /// Méthode pour forcer une mise à jour complète (rafraîchissement) Future forceRefreshAllData(BuildContext context) async { debugPrint("$_logMain Forçage de la mise à jour de toutes les données..."); - // Vérifier si des adresses de wallet sont disponibles + // Vérifier si des adresses de wallet sont disponibles - // Charger les adresses de wallet - await loadWalletsAddresses(); + // Charger les adresses de wallet + await loadWalletsAddresses(); if (evmAddresses.isEmpty) { - debugPrint("$_logWarning updateMainInformations : aucune adresse de wallet disponible"); + debugPrint( + "$_logWarning updateMainInformations : aucune adresse de wallet disponible"); return; } @@ -867,22 +890,24 @@ class DataManager extends ChangeNotifier { Future loadWalletBalanceHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de l'historique des balances..."); - + try { var box = Hive.box('balanceHistory'); - List? balanceHistoryJson = box.get('balanceHistory_totalWalletValue'); + List? balanceHistoryJson = + box.get('balanceHistory_totalWalletValue'); // Convertir chaque élément JSON en objet BalanceRecord et l'ajouter à walletBalanceHistory walletBalanceHistory = balanceHistoryJson != null ? balanceHistoryJson - .map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))) + .map((recordJson) => + BalanceRecord.fromJson(Map.from(recordJson))) .toList() : []; // Si l'historique est vide, on ajoute la valeur actuelle if (walletBalanceHistory.isEmpty) { walletBalanceHistory.add(BalanceRecord( - balance: totalWalletValue, + balance: totalWalletValue, timestamp: DateTime.now(), tokenType: 'totalWalletValue')); saveWalletBalanceHistory(); @@ -890,31 +915,36 @@ class DataManager extends ChangeNotifier { // Assigner à balanceHistory (utilisée pour les calculs d'APY) aussi balanceHistory = List.from(walletBalanceHistory); - + // Calculer l'APY après chargement de l'historique si nous avons suffisamment de données if (balanceHistory.length >= 2) { try { // Calcul de l'APY déplacé vers calculateApyValues // Nous ne calculons pas l'APY ici, mais attendons que toutes les données soient chargées - debugPrint("$_logTask Historique de balance chargé, APY sera calculé quand toutes les données seront disponibles"); + debugPrint( + "$_logTask Historique de balance chargé, APY sera calculé quand toutes les données seront disponibles"); } catch (e) { - debugPrint("$_logError Erreur lors du traitement initial de l'historique: $e"); + debugPrint( + "$_logError Erreur lors du traitement initial de l'historique: $e"); } } else { - debugPrint("$_logWarning Historique insuffisant pour calculer l'APY: ${balanceHistory.length} enregistrement(s)"); + debugPrint( + "$_logWarning Historique insuffisant pour calculer l'APY: ${balanceHistory.length} enregistrement(s)"); } final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique de balance chargé: ${walletBalanceHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique de balance chargé: ${walletBalanceHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de l'historique de balance: $e"); + debugPrint( + "$_logError Erreur lors du chargement de l'historique de balance: $e"); } } Future loadRentedHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de l'historique des locations..."); - + try { var box = Hive.box('rentedArchive'); List? rentedHistoryJson = box.get('rented_history'); @@ -931,43 +961,49 @@ class DataManager extends ChangeNotifier { notifyListeners(); final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique de location chargé: ${rentedHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique de location chargé: ${rentedHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de l'historique de location: $e"); + debugPrint( + "$_logError Erreur lors du chargement de l'historique de location: $e"); } } Future loadRoiHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de l'historique ROI..."); - + try { var box = Hive.box('roiValueArchive'); List? roiHistoryJson = box.get('roi_history'); // Vérifier si les données sont nulles if (roiHistoryJson == null) { - debugPrint("$_logWarning Aucun historique ROI trouvé, initialisation avec liste vide"); + debugPrint( + "$_logWarning Aucun historique ROI trouvé, initialisation avec liste vide"); roiHistory = []; return; } - + // Convertir les données JSON en objets ROIRecord try { roiHistory = roiHistoryJson.map((recordJson) { return ROIRecord.fromJson(Map.from(recordJson)); }).toList(); } catch (e) { - debugPrint("$_logError Erreur lors de la conversion des enregistrements ROI: $e"); + debugPrint( + "$_logError Erreur lors de la conversion des enregistrements ROI: $e"); roiHistory = []; } notifyListeners(); final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique ROI chargé: ${roiHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique ROI chargé: ${roiHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de l'historique ROI: $e"); + debugPrint( + "$_logError Erreur lors du chargement de l'historique ROI: $e"); roiHistory = []; } } @@ -975,7 +1011,7 @@ class DataManager extends ChangeNotifier { Future loadApyHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de l'historique APY..."); - + try { // Utiliser la boîte correcte qui est ouverte dans main.dart var box = Hive.box('apyValueArchive'); @@ -997,47 +1033,52 @@ class DataManager extends ChangeNotifier { } else if (recordJson is Map) { recordMap = Map.from(recordJson); } else { - debugPrint("$_logWarning Format de données APY invalide ignoré: $recordJson"); + debugPrint( + "$_logWarning Format de données APY invalide ignoré: $recordJson"); continue; } - + // Gestion spéciale du timestamp if (recordMap.containsKey('timestamp')) { var timestampValue = recordMap['timestamp']; DateTime parsedTimestamp; - + try { if (timestampValue is int) { // Timestamp en millisecondes - parsedTimestamp = DateTime.fromMillisecondsSinceEpoch(timestampValue); + parsedTimestamp = + DateTime.fromMillisecondsSinceEpoch(timestampValue); } else if (timestampValue is double) { // Timestamp en millisecondes (format double) - parsedTimestamp = DateTime.fromMillisecondsSinceEpoch(timestampValue.toInt()); + parsedTimestamp = + DateTime.fromMillisecondsSinceEpoch(timestampValue.toInt()); } else if (timestampValue is String) { // Essayer de parser comme timestamp en millisecondes d'abord try { int timestampMs = int.parse(timestampValue); - parsedTimestamp = DateTime.fromMillisecondsSinceEpoch(timestampMs); + parsedTimestamp = + DateTime.fromMillisecondsSinceEpoch(timestampMs); } catch (e) { // Si ça échoue, essayer de parser comme date ISO parsedTimestamp = DateTime.parse(timestampValue); } } else { - debugPrint("$_logWarning Type de timestamp non supporté: ${timestampValue.runtimeType}"); + debugPrint( + "$_logWarning Type de timestamp non supporté: ${timestampValue.runtimeType}"); continue; } - + // Remplacer le timestamp dans recordMap avec le DateTime parsé recordMap['timestamp'] = parsedTimestamp.toIso8601String(); } catch (e) { - debugPrint("$_logWarning Erreur lors du parsing du timestamp: $timestampValue, erreur: $e"); + debugPrint( + "$_logWarning Erreur lors du parsing du timestamp: $timestampValue, erreur: $e"); continue; } } - + // Validation et conversion sécurisée des types pour les valeurs APY if (recordMap.containsKey('apy') || recordMap.containsKey('netApy')) { - // Convertir les valeurs String en double si nécessaire if (recordMap['apy'] is String) { recordMap['apy'] = double.tryParse(recordMap['apy']) ?? 0.0; @@ -1046,15 +1087,18 @@ class DataManager extends ChangeNotifier { recordMap['netApy'] = double.tryParse(recordMap['netApy']) ?? 0.0; } if (recordMap['grossApy'] is String) { - recordMap['grossApy'] = double.tryParse(recordMap['grossApy']) ?? 0.0; + recordMap['grossApy'] = + double.tryParse(recordMap['grossApy']) ?? 0.0; } - + apyHistory.add(APYRecord.fromJson(recordMap)); } else { - debugPrint("$_logWarning Données APY incomplètes ignorées: $recordMap"); + debugPrint( + "$_logWarning Données APY incomplètes ignorées: $recordMap"); } } catch (e) { - debugPrint("$_logWarning Erreur lors du traitement d'un enregistrement APY: $e"); + debugPrint( + "$_logWarning Erreur lors du traitement d'un enregistrement APY: $e"); debugPrint("$_logWarning Données problématiques: $recordJson"); continue; } @@ -1063,9 +1107,11 @@ class DataManager extends ChangeNotifier { notifyListeners(); final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique APY chargé: ${apyHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique APY chargé: ${apyHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de l'historique APY: $e"); + debugPrint( + "$_logError Erreur lors du chargement de l'historique APY: $e"); // Initialiser avec une liste vide en cas d'erreur apyHistory = []; } @@ -1074,7 +1120,7 @@ class DataManager extends ChangeNotifier { Future loadHealthAndLtvHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Chargement de l'historique Health & LTV..."); - + try { var box = Hive.box('HealthAndLtvValueArchive'); List? healthAndLtvHistoryJson = box.get('healthAndLtv_history'); @@ -1086,15 +1132,18 @@ class DataManager extends ChangeNotifier { // Charger l'historique healthAndLtvHistory = healthAndLtvHistoryJson.map((recordJson) { - return HealthAndLtvRecord.fromJson(Map.from(recordJson)); + return HealthAndLtvRecord.fromJson( + Map.from(recordJson)); }).toList(); notifyListeners(); final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique Health & LTV chargé: ${healthAndLtvHistory.length} entrées (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique Health & LTV chargé: ${healthAndLtvHistory.length} entrées (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du chargement de l'historique Health & LTV: $e"); + debugPrint( + "$_logError Erreur lors du chargement de l'historique Health & LTV: $e"); } } @@ -1102,27 +1151,29 @@ class DataManager extends ChangeNotifier { Future saveWalletBalanceHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Sauvegarde de l'historique des balances..."); - + try { var box = Hive.box('walletValueArchive'); - + // Convertir les données en format JSON List> balanceHistoryJson = walletBalanceHistory.map((record) => record.toJson()).toList(); - + // Sauvegarder dans Hive await box.put('balanceHistory_totalWalletValue', balanceHistoryJson); - + // S'assurer que les données dans balanceHistory sont aussi à jour balanceHistory = List.from(walletBalanceHistory); - + // Mise à jour également dans la boîte 'balanceHistory' pour assurer la cohérence var boxBalance = Hive.box('balanceHistory'); - await boxBalance.put('balanceHistory_totalWalletValue', balanceHistoryJson); - + await boxBalance.put( + 'balanceHistory_totalWalletValue', balanceHistoryJson); + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique des balances sauvegardé: ${walletBalanceHistory.length} entrées (${duration.inMilliseconds}ms)"); - + debugPrint( + "$_logSuccess Historique des balances sauvegardé: ${walletBalanceHistory.length} entrées (${duration.inMilliseconds}ms)"); + notifyListeners(); } catch (e) { debugPrint("$_logError Erreur lors de la sauvegarde de l'historique: $e"); @@ -1132,19 +1183,21 @@ class DataManager extends ChangeNotifier { Future saveRentedHistory() async { final startTime = DateTime.now(); debugPrint("$_logTask Sauvegarde de l'historique des locations..."); - + try { var box = Hive.box('rentedArchive'); List> rentedHistoryJson = rentedHistory.map((record) => record.toJson()).toList(); await box.put('rented_history', rentedHistoryJson); - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique des locations sauvegardé (${duration.inMilliseconds}ms)"); - + debugPrint( + "$_logSuccess Historique des locations sauvegardé (${duration.inMilliseconds}ms)"); + notifyListeners(); } catch (e) { - debugPrint("$_logError Erreur lors de la sauvegarde de l'historique des locations: $e"); + debugPrint( + "$_logError Erreur lors de la sauvegarde de l'historique des locations: $e"); } } @@ -1152,7 +1205,7 @@ class DataManager extends ChangeNotifier { void addAddressesForUserId(String userId, List addresses) { final startTime = DateTime.now(); debugPrint("$_logTask Ajout d'adresses pour userId: $userId..."); - + try { if (userIdToAddresses.containsKey(userId)) { userIdToAddresses[userId]!.addAll(addresses); @@ -1160,13 +1213,15 @@ class DataManager extends ChangeNotifier { userIdToAddresses[userId] = addresses; } saveUserIdToAddresses(); - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Adresses ajoutées pour userId: $userId (${duration.inMilliseconds}ms)"); - + debugPrint( + "$_logSuccess Adresses ajoutées pour userId: $userId (${duration.inMilliseconds}ms)"); + notifyListeners(); } catch (e) { - debugPrint("$_logError Erreur lors de l'ajout d'adresses pour userId: $e"); + debugPrint( + "$_logError Erreur lors de l'ajout d'adresses pour userId: $e"); } } @@ -1261,23 +1316,21 @@ class DataManager extends ChangeNotifier { realToken['totalTokens'] > 0 && realToken['fullName'] != null && !realToken['fullName'].startsWith('OLD-') && - realToken['uuid'].toLowerCase() != Parameters.rwaTokenAddress.toLowerCase()) { + realToken['uuid'].toLowerCase() != + Parameters.rwaTokenAddress.toLowerCase()) { double? customInitPrice = customInitPrices[tokenContractAddress]; double initPrice = customInitPrice ?? (realToken['historic']['init_price'] as num?)?.toDouble() ?? 0.0; - - String fullName = realToken['fullName']; String country = LocationUtils.extractCountry(fullName); String regionCode = LocationUtils.extractRegion(fullName); String city = LocationUtils.extractCity(fullName); // Récupérer les loyers cumulés pour ce token - double totalRentReceived = cumulativeRentsByToken[tokenContractAddress] ?? 0.0; - - + double totalRentReceived = + cumulativeRentsByToken[tokenContractAddress] ?? 0.0; allTokensList.add({ 'uuid': tokenContractAddress, @@ -1397,10 +1450,12 @@ class DataManager extends ChangeNotifier { // Méthode pour récupérer et calculer les données pour le Dashboard et Portfolio Future fetchAndCalculateData({bool forceFetch = false}) async { -debugPrint("🗃️ Début récupération et calcul des données pour le Dashboard et Portfolio"); - // Vérifier si des adresses de wallet sont disponibles + debugPrint( + "🗃️ Début récupération et calcul des données pour le Dashboard et Portfolio"); + // Vérifier si des adresses de wallet sont disponibles if (evmAddresses.isEmpty) { - debugPrint("$_logWarning updateMainInformations : aucune adresse de wallet disponible"); + debugPrint( + "$_logWarning updateMainInformations : aucune adresse de wallet disponible"); return; } @@ -1466,7 +1521,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa for (var realToken in realTokens.cast>()) { realTokensIndex[realToken['uuid'].toLowerCase()] = realToken; } - + Map> yamHistoryIndex = {}; for (var yam in yamHistory) { yamHistoryIndex[yam['id'].toLowerCase()] = yam; @@ -1526,22 +1581,24 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa double? customInitPrice = customInitPrices[tokenContractAddress]; double initPrice = customInitPrice ?? ((matchingRealToken['historic']['init_price'] as num?)?.toDouble() ?? - 0.0); - + 0.0); + // Vérification des transactions pour calculer un prix moyen si customInitPrice est null - if (customInitPrice == null && transactionsByToken.containsKey(tokenContractAddress) && transactionsByToken[tokenContractAddress]!.isNotEmpty) { - List> tokenTransactions = transactionsByToken[tokenContractAddress]!; + if (customInitPrice == null && + transactionsByToken.containsKey(tokenContractAddress) && + transactionsByToken[tokenContractAddress]!.isNotEmpty) { + List> tokenTransactions = + transactionsByToken[tokenContractAddress]!; double totalWeightedPrice = 0.0; double totalQuantity = 0.0; int transactionCount = 0; - + for (var transaction in tokenTransactions) { - if (transaction['price'] != null && + if (transaction['price'] != null && transaction['price'] > 0 && transaction['transactionType'] != transactionTypeTransfer && transaction['amount'] != null && transaction['amount'] > 0) { - double price = transaction['price']; double amount = transaction['amount']; totalWeightedPrice += price * amount; @@ -1549,52 +1606,57 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa transactionCount++; } } - + if (transactionCount > 0 && totalQuantity > 0) { double weightedAveragePrice = totalWeightedPrice / totalQuantity; initPrice = weightedAveragePrice; } } - // Parsing du fullName pour obtenir country, regionCode et city - final nameDetails = parseFullName(matchingRealToken['fullName']); - - // Récupération des données Yam avec index optimisé - final yamData = yamHistoryIndex[tokenContractAddress] ?? {}; - final double yamTotalVolume = yamData['totalVolume'] ?? 1.0; - final double yamAverageValue = - (yamData['averageValue'] != null && yamData['averageValue'] != 0) - ? yamData['averageValue'] - : tokenPrice; - - // Fusion dans le portfolio par token (agrégation si le même token apparaît plusieurs fois) - int index = newPortfolio.indexWhere((item) => item['uuid'] == tokenContractAddress); - if (index != -1) { - Map existingItem = newPortfolio[index]; - List wallets = existingItem['wallets'] is List - ? List.from(existingItem['wallets']) - : []; - if (!wallets.contains(walletToken['wallet'])) { - wallets.add(walletToken['wallet']); - // Log dès qu'un nouveau wallet est ajouté pour ce token - } - existingItem['wallets'] += wallets; - existingItem['amount'] += walletToken['amount']; - existingItem['totalValue'] = existingItem['amount'] * tokenPrice; - existingItem['initialTotalValue'] = existingItem['amount'] * initPrice; - existingItem['dailyIncome'] = matchingRealToken['netRentDayPerToken'] * existingItem['amount']; - existingItem['monthlyIncome'] = matchingRealToken['netRentMonthPerToken'] * existingItem['amount']; - existingItem['yearlyIncome'] = matchingRealToken['netRentYearPerToken'] * existingItem['amount']; - } else { - Map portfolioItem = { - 'id': matchingRealToken['id'], - 'uuid': tokenContractAddress, - 'shortName': matchingRealToken['shortName'], - 'fullName': matchingRealToken['fullName'], - 'country': nameDetails['country'], - 'regionCode': nameDetails['regionCode'], - 'city': nameDetails['city'], - 'imageLink': matchingRealToken['imageLink'], + // Parsing du fullName pour obtenir country, regionCode et city + final nameDetails = parseFullName(matchingRealToken['fullName']); + + // Récupération des données Yam avec index optimisé + final yamData = + yamHistoryIndex[tokenContractAddress] ?? {}; + final double yamTotalVolume = yamData['totalVolume'] ?? 1.0; + final double yamAverageValue = + (yamData['averageValue'] != null && yamData['averageValue'] != 0) + ? yamData['averageValue'] + : tokenPrice; + + // Fusion dans le portfolio par token (agrégation si le même token apparaît plusieurs fois) + int index = newPortfolio + .indexWhere((item) => item['uuid'] == tokenContractAddress); + if (index != -1) { + Map existingItem = newPortfolio[index]; + List wallets = existingItem['wallets'] is List + ? List.from(existingItem['wallets']) + : []; + if (!wallets.contains(walletToken['wallet'])) { + wallets.add(walletToken['wallet']); + // Log dès qu'un nouveau wallet est ajouté pour ce token + } + existingItem['wallets'] += wallets; + existingItem['amount'] += walletToken['amount']; + existingItem['totalValue'] = existingItem['amount'] * tokenPrice; + existingItem['initialTotalValue'] = existingItem['amount'] * initPrice; + existingItem['dailyIncome'] = + matchingRealToken['netRentDayPerToken'] * existingItem['amount']; + existingItem['monthlyIncome'] = + matchingRealToken['netRentMonthPerToken'] * existingItem['amount']; + existingItem['yearlyIncome'] = + matchingRealToken['netRentYearPerToken'] * existingItem['amount']; + } else { + Map portfolioItem = { + 'id': matchingRealToken['id'], + 'uuid': tokenContractAddress, + 'shortName': matchingRealToken['shortName'], + 'fullName': matchingRealToken['fullName'], + 'country': nameDetails['country'], + 'regionCode': nameDetails['regionCode'], + 'city': nameDetails['city'], + 'imageLink': matchingRealToken['imageLink'], 'lat': matchingRealToken['coordinate']['lat'], 'lng': matchingRealToken['coordinate']['lng'], 'amount': walletToken['amount'], @@ -1662,7 +1724,8 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Mise à jour du loyer total pour ce token if (tokenAddress.isNotEmpty) { // Utiliser directement la valeur précalculée au lieu d'appeler getRentDetailsForToken - double rentDetails = cumulativeRentsByToken[tokenAddress.toLowerCase()] ?? 0.0; + double rentDetails = + cumulativeRentsByToken[tokenAddress.toLowerCase()] ?? 0.0; int index = newPortfolio.indexWhere((item) => item['uuid'] == tokenAddress); if (index != -1) { @@ -1720,8 +1783,9 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // -------- Calcul de la valeur RMM par wallet -------- Map walletRmmValues = {}; - Map walletRmmTokensSum = {}; // Pour compter le nombre de tokens en RMM - + Map walletRmmTokensSum = + {}; // Pour compter le nombre de tokens en RMM + for (var token in walletTokens) { // On considère ici uniquement les tokens de type RMM (donc différents de "wallet") if (token['type'] != "wallet") { @@ -1739,12 +1803,13 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Cumuler la valeur RMM pour ce wallet walletRmmValues[wallet] = (walletRmmValues[wallet] ?? 0.0) + tokenValue; // Cumuler le nombre de tokens en RMM - walletRmmTokensSum[wallet] = (walletRmmTokensSum[wallet] ?? 0.0) + token['amount']; + walletRmmTokensSum[wallet] = + (walletRmmTokensSum[wallet] ?? 0.0) + token['amount']; } } // Stocker ces valeurs dans une variable accessible (par exemple, dans DataManager) perWalletRmmValues = walletRmmValues; - + // Mettre à jour les statistiques des wallets avec les valeurs RMM for (var stat in walletStats) { final String address = stat['address'] as String; @@ -1785,30 +1850,34 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa uniqueWalletTokens.intersection(uniqueRmmTokens).length; _portfolio = newPortfolio; - + // Calculer le ROI global double totalRent = getTotalRentReceived(); - if (initialTotalValue > 0.000001) { // Vérifier si initialTotalValue n'est pas trop proche de 0 + if (initialTotalValue > 0.000001) { + // Vérifier si initialTotalValue n'est pas trop proche de 0 roiGlobalValue = totalRent / initialTotalValue * 100; // Limiter le ROI à une valeur maximale raisonnable (par exemple 3650%) - if (roiGlobalValue.isInfinite || roiGlobalValue.isNaN || roiGlobalValue > 3650) { + if (roiGlobalValue.isInfinite || + roiGlobalValue.isNaN || + roiGlobalValue > 3650) { roiGlobalValue = 3650; } } else { roiGlobalValue = 0.0; } - + // Archiver uniquement si nous avons des données de loyer if (rentData.isNotEmpty && totalRent > 0) { debugPrint("💾 Archivage de la valeur ROI: $roiGlobalValue"); _archiveManager.archiveRoiValue(roiGlobalValue); } else { - debugPrint("⚠️ Pas d'archivage ROI: liste des loyers vide ou montant total des loyers nul"); + debugPrint( + "⚠️ Pas d'archivage ROI: liste des loyers vide ou montant total des loyers nul"); } // Calculer l'APY uniquement si toutes les données nécessaires sont disponibles safeCalculateApyValues(); - + healthFactor = (rmmValue * 0.7) / (totalUsdcBorrowBalance + totalXdaiBorrowBalance); ltv = ((totalUsdcBorrowBalance + totalXdaiBorrowBalance) / rmmValue * 100); @@ -1861,7 +1930,6 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa if (token.containsKey('update30') && token['update30'] is List && token['update30'].isNotEmpty) { - // Récupérer les informations de base du token final String shortName = token['shortName'] ?? 'Nom inconnu'; final String imageLink = @@ -1881,7 +1949,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Ajouter les mises à jour extraites dans recentUpdates recentUpdates.addAll(updatesWithDetails); - } + } } // Trier les mises à jour par date @@ -1959,51 +2027,57 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa }); } - - Future processTransactionsHistory( - BuildContext context, - List> transactionsHistory, - List> yamTransactions) async { - - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final Set evmAddresses = Set.from(prefs.getStringList('evmAddresses') ?? {}); - - Map>> tempTransactionsByToken = {}; - - for (var transaction in transactionsHistory) { - final String? tokenId = transaction['Token ID']?.toLowerCase(); - final String? timestampStr = transaction['timestamp']; - final double? amount = (transaction['amount'] as num?)?.toDouble(); - final String? sender = transaction['sender']?.toLowerCase(); - final String? transactionId = transaction['Transaction ID']?.toLowerCase(); - - if (tokenId == null || timestampStr == null || amount == null || transactionId == null) { - continue; - } - - try { - // ✅ Convertir le timestamp Unix en DateTime - final int timestampMs; - try { - timestampMs = int.parse(timestampStr) * 1000; // Convertir en millisecondes - } catch (e) { + BuildContext context, + List> transactionsHistory, + List> yamTransactions) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final Set evmAddresses = + Set.from(prefs.getStringList('evmAddresses') ?? {}); + + Map>> tempTransactionsByToken = {}; + + for (var transaction in transactionsHistory) { + final String? tokenId = transaction['Token ID']?.toLowerCase(); + final String? timestampStr = transaction['timestamp']; + final double? amount = (transaction['amount'] as num?)?.toDouble(); + final String? sender = transaction['sender']?.toLowerCase(); + final String? transactionId = + transaction['Transaction ID']?.toLowerCase(); + + if (tokenId == null || + timestampStr == null || + amount == null || + transactionId == null) { continue; } - DateTime dateTime; try { - dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs, isUtc: true); - } catch (e) { - continue; - } + // ✅ Convertir le timestamp Unix en DateTime + final int timestampMs; + try { + timestampMs = + int.parse(timestampStr) * 1000; // Convertir en millisecondes + } catch (e) { + continue; + } - final bool isInternalTransfer = evmAddresses.contains(sender); - // Utiliser les textes capturés au lieu de S.of(context) - String transactionType = isInternalTransfer ? transactionTypeTransfer : transactionTypePurchase; + DateTime dateTime; + try { + dateTime = + DateTime.fromMillisecondsSinceEpoch(timestampMs, isUtc: true); + } catch (e) { + continue; + } - try { - final matchingYamTransaction = yamTransactions.firstWhere( + final bool isInternalTransfer = evmAddresses.contains(sender); + // Utiliser les textes capturés au lieu de S.of(context) + String transactionType = isInternalTransfer + ? transactionTypeTransfer + : transactionTypePurchase; + + try { + final matchingYamTransaction = yamTransactions.firstWhere( (yamTransaction) { final String? yamId = yamTransaction['transaction_id']?.toLowerCase(); @@ -2015,87 +2089,101 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa orElse: () => {}, ); - double? price; - if (matchingYamTransaction.isNotEmpty) { - final double? rawPrice = (matchingYamTransaction['price'] as num?)?.toDouble(); - price = rawPrice ?? 0.0; - // Utiliser le texte capturé pour YAM - transactionType = transactionTypeYam; - } else { - // Ajouter un prix par défaut pour les transactions d'achat - if (transactionType == transactionTypePurchase) { - // Chercher le token dans realTokens pour obtenir un prix initial - final matchingRealToken = realTokens.cast>().firstWhere( - (rt) => rt['uuid'].toLowerCase() == tokenId, - orElse: () => {}, - ); - - if (matchingRealToken.isNotEmpty) { - // Utiliser tokenPrice au lieu du prix initial - price = (matchingRealToken['tokenPrice'] as num?)?.toDouble() ?? - (matchingRealToken['historic']?['init_price'] as num?)?.toDouble() ?? - 0.0; - } else { - price = 0.0; // Prix par défaut si aucune information n'est disponible + double? price; + if (matchingYamTransaction.isNotEmpty) { + final double? rawPrice = + (matchingYamTransaction['price'] as num?)?.toDouble(); + price = rawPrice ?? 0.0; + // Utiliser le texte capturé pour YAM + transactionType = transactionTypeYam; + } else { + // Ajouter un prix par défaut pour les transactions d'achat + if (transactionType == transactionTypePurchase) { + // Chercher le token dans realTokens pour obtenir un prix initial + final matchingRealToken = + realTokens.cast>().firstWhere( + (rt) => rt['uuid'].toLowerCase() == tokenId, + orElse: () => {}, + ); + + if (matchingRealToken.isNotEmpty) { + // Utiliser tokenPrice au lieu du prix initial + price = (matchingRealToken['tokenPrice'] as num?)?.toDouble() ?? + (matchingRealToken['historic']?['init_price'] as num?) + ?.toDouble() ?? + 0.0; + } else { + price = + 0.0; // Prix par défaut si aucune information n'est disponible + } } } - } - tempTransactionsByToken.putIfAbsent(tokenId, () => []).add({ - "amount": amount, - "dateTime": dateTime, - "transactionType": transactionType, - "price": price, - }); + tempTransactionsByToken.putIfAbsent(tokenId, () => []).add({ + "amount": amount, + "dateTime": dateTime, + "transactionType": transactionType, + "price": price, + }); + } catch (e) { + debugPrint("⚠️ Erreur lors du traitement des informations YAM: $e"); + continue; + } } catch (e) { - debugPrint("⚠️ Erreur lors du traitement des informations YAM: $e"); + debugPrint( + "⚠️ Erreur de parsing de la transaction: $transaction. Détail: $e"); continue; } - } catch (e) { - debugPrint("⚠️ Erreur de parsing de la transaction: $transaction. Détail: $e"); - continue; } - } - // ✅ **Ajout des transactions YAM manquantes** - for (var yamTransaction in yamTransactions) { - final String? yamId = yamTransaction['transaction_id']?.toLowerCase(); - if (yamId == null || yamId.isEmpty) continue; + // ✅ **Ajout des transactions YAM manquantes** + for (var yamTransaction in yamTransactions) { + final String? yamId = yamTransaction['transaction_id']?.toLowerCase(); + if (yamId == null || yamId.isEmpty) continue; - final String yamIdTrimmed = yamId.substring(0, yamId.length - 10); - final bool alreadyExists = transactionsHistory.any((transaction) => - transaction['Transaction ID']?.toLowerCase().startsWith(yamIdTrimmed) ?? false); + final String yamIdTrimmed = yamId.substring(0, yamId.length - 10); + final bool alreadyExists = transactionsHistory.any((transaction) => + transaction['Transaction ID'] + ?.toLowerCase() + .startsWith(yamIdTrimmed) ?? + false); - if (!alreadyExists) { - final String? yamTimestamp = yamTransaction['timestamp']; - final double? yamPrice = (yamTransaction['price'] as num?)?.toDouble(); - final double? yamQuantity = (yamTransaction['quantity'] as num?)?.toDouble(); - final String? offerTokenAddress = yamTransaction['offer_token_address']?.toLowerCase(); + if (!alreadyExists) { + final String? yamTimestamp = yamTransaction['timestamp']; + final double? yamPrice = (yamTransaction['price'] as num?)?.toDouble(); + final double? yamQuantity = + (yamTransaction['quantity'] as num?)?.toDouble(); + final String? offerTokenAddress = + yamTransaction['offer_token_address']?.toLowerCase(); - if (yamTimestamp == null || yamPrice == null || yamQuantity == null || offerTokenAddress == null) { - continue; - } + if (yamTimestamp == null || + yamPrice == null || + yamQuantity == null || + offerTokenAddress == null) { + continue; + } - final int timestampMs; - try { - timestampMs = int.parse(yamTimestamp) * 1000; - } catch (e) { - continue; - } + final int timestampMs; + try { + timestampMs = int.parse(yamTimestamp) * 1000; + } catch (e) { + continue; + } - tempTransactionsByToken.putIfAbsent(offerTokenAddress, () => []).add({ - "amount": yamQuantity, - "dateTime": DateTime.fromMillisecondsSinceEpoch(timestampMs, isUtc: true), - "transactionType": transactionTypeYam, - "price": yamPrice, - }); + tempTransactionsByToken.putIfAbsent(offerTokenAddress, () => []).add({ + "amount": yamQuantity, + "dateTime": + DateTime.fromMillisecondsSinceEpoch(timestampMs, isUtc: true), + "transactionType": transactionTypeYam, + "price": yamPrice, + }); + } } - } - debugPrint("✅ Fin du traitement des transactions."); - transactionsByToken.addAll(tempTransactionsByToken); - isLoadingTransactions = false; -} + debugPrint("✅ Fin du traitement des transactions."); + transactionsByToken.addAll(tempTransactionsByToken); + isLoadingTransactions = false; + } // Méthode pour récupérer les données des propriétés Future fetchPropertyData({bool forceFetch = false}) async { @@ -2113,22 +2201,25 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Parcourir chaque token du portefeuille for (var token in walletTokens) { if (token['token'] == null) continue; - + final String tokenAddress = token['token'].toLowerCase(); - + // Correspondre avec les RealTokens - final matchingRealToken = realTokens.cast>().firstWhere( - (realToken) => realToken['uuid'].toLowerCase() == tokenAddress, - orElse: () => {}, - ); + final matchingRealToken = + realTokens.cast>().firstWhere( + (realToken) => realToken['uuid'].toLowerCase() == tokenAddress, + orElse: () => {}, + ); - if (matchingRealToken.isNotEmpty && matchingRealToken['propertyType'] != null) { + if (matchingRealToken.isNotEmpty && + matchingRealToken['propertyType'] != null) { final int propertyType = matchingRealToken['propertyType']; tokenProcessed++; - + // Vérifiez si le type de propriété existe déjà dans propertyData - final existingIndex = tempPropertyData.indexWhere((data) => data['propertyType'] == propertyType); - + final existingIndex = tempPropertyData + .indexWhere((data) => data['propertyType'] == propertyType); + if (existingIndex >= 0) { // Incrémenter le compte si la propriété existe déjà tempPropertyData[existingIndex]['count'] += 1; @@ -2138,7 +2229,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } } } - + propertyData = tempPropertyData; notifyListeners(); } @@ -2210,104 +2301,111 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa debugPrint('Toutes les données ont été réinitialisées.'); } - Future fetchRmmBalances() async { - try { - // Totaux globaux - double totalUsdcDepositSum = 0; - double totalUsdcBorrowSum = 0; - double totalXdaiDepositSum = 0; - double totalXdaiBorrowSum = 0; - double totalGnosisUsdcSum = 0; - double totalGnosisXdaiSum = 0; - double totalGnosisRegSum = 0; - double totalGnosisVaultRegSum = 0; - // Liste pour stocker les données par wallet - List> walletDetails = []; - - String? timestamp; - - // Itérer sur chaque balance (chaque wallet) - for (var balance in rmmBalances) { - double usdcDeposit = balance['usdcDepositBalance']; - double usdcBorrow = balance['usdcBorrowBalance']; - double xdaiDeposit = balance['xdaiDepositBalance']; - double xdaiBorrow = balance['xdaiBorrowBalance']; - double gnosisUsdc = balance['gnosisUsdcBalance']; - double gnosisXdai = balance['gnosisXdaiBalance']; - double gnosisReg = balance['gnosisRegBalance']; - double gnosisVaultReg = balance['gnosisVaultRegBalance']; - timestamp = balance['timestamp']; // Dernier timestamp mis à jour - - // Mise à jour des totaux globaux - totalUsdcDepositSum += usdcDeposit; - totalUsdcBorrowSum += usdcBorrow; - totalXdaiDepositSum += xdaiDeposit; - totalXdaiBorrowSum += xdaiBorrow; - totalGnosisUsdcSum += gnosisUsdc; - totalGnosisXdaiSum += gnosisXdai; - totalGnosisRegSum += gnosisReg; - totalGnosisVaultRegSum += gnosisVaultReg; - // Stocker les données propres au wallet - walletDetails.add({ - 'address': balance['address'], - 'usdcDeposit': usdcDeposit, - 'usdcBorrow': usdcBorrow, - 'xdaiDeposit': xdaiDeposit, - 'xdaiBorrow': xdaiBorrow, - 'gnosisUsdc': gnosisUsdc, - 'gnosisReg': gnosisReg, - 'gnosisVaultReg': gnosisVaultReg, - 'gnosisXdai': gnosisXdai, - 'timestamp': timestamp, - }); - } - - // Mise à jour des variables globales avec les totaux cumulés - totalUsdcDepositBalance = totalUsdcDepositSum; - totalUsdcBorrowBalance = totalUsdcBorrowSum; - totalXdaiDepositBalance = totalXdaiDepositSum; - totalXdaiBorrowBalance = totalXdaiBorrowSum; - gnosisUsdcBalance = totalGnosisUsdcSum; - gnosisRegBalance = totalGnosisRegSum; - gnosisXdaiBalance = totalGnosisXdaiSum; - gnosisVaultRegBalance = totalGnosisVaultRegSum; - // Stocker les détails par wallet - perWalletBalances = walletDetails; - - // Calcul de l'APY GLOBAL uniquement après avoir accumulé les totaux + Future fetchRmmBalances() async { try { - usdcDepositApy = await calculateAPY('usdcDeposit'); - usdcBorrowApy = await calculateAPY('usdcBorrow'); - xdaiDepositApy = await calculateAPY('xdaiDeposit'); - xdaiBorrowApy = await calculateAPY('xdaiBorrow'); - } catch (e) { - debugPrint('Erreur lors du calcul de l\'APY global: $e'); - } + // Totaux globaux + double totalUsdcDepositSum = 0; + double totalUsdcBorrowSum = 0; + double totalXdaiDepositSum = 0; + double totalXdaiBorrowSum = 0; + double totalGnosisUsdcSum = 0; + double totalGnosisXdaiSum = 0; + double totalGnosisRegSum = 0; + double totalGnosisVaultRegSum = 0; + // Liste pour stocker les données par wallet + List> walletDetails = []; + + String? timestamp; + + // Itérer sur chaque balance (chaque wallet) + for (var balance in rmmBalances) { + double usdcDeposit = balance['usdcDepositBalance']; + double usdcBorrow = balance['usdcBorrowBalance']; + double xdaiDeposit = balance['xdaiDepositBalance']; + double xdaiBorrow = balance['xdaiBorrowBalance']; + double gnosisUsdc = balance['gnosisUsdcBalance']; + double gnosisXdai = balance['gnosisXdaiBalance']; + double gnosisReg = balance['gnosisRegBalance']; + double gnosisVaultReg = balance['gnosisVaultRegBalance']; + timestamp = balance['timestamp']; // Dernier timestamp mis à jour + + // Mise à jour des totaux globaux + totalUsdcDepositSum += usdcDeposit; + totalUsdcBorrowSum += usdcBorrow; + totalXdaiDepositSum += xdaiDeposit; + totalXdaiBorrowSum += xdaiBorrow; + totalGnosisUsdcSum += gnosisUsdc; + totalGnosisXdaiSum += gnosisXdai; + totalGnosisRegSum += gnosisReg; + totalGnosisVaultRegSum += gnosisVaultReg; + // Stocker les données propres au wallet + walletDetails.add({ + 'address': balance['address'], + 'usdcDeposit': usdcDeposit, + 'usdcBorrow': usdcBorrow, + 'xdaiDeposit': xdaiDeposit, + 'xdaiBorrow': xdaiBorrow, + 'gnosisUsdc': gnosisUsdc, + 'gnosisReg': gnosisReg, + 'gnosisVaultReg': gnosisVaultReg, + 'gnosisXdai': gnosisXdai, + 'timestamp': timestamp, + }); + } - notifyListeners(); // Notifier l'interface que les données ont été mises à jour + // Mise à jour des variables globales avec les totaux cumulés + totalUsdcDepositBalance = totalUsdcDepositSum; + totalUsdcBorrowBalance = totalUsdcBorrowSum; + totalXdaiDepositBalance = totalXdaiDepositSum; + totalXdaiBorrowBalance = totalXdaiBorrowSum; + gnosisUsdcBalance = totalGnosisUsdcSum; + gnosisRegBalance = totalGnosisRegSum; + gnosisXdaiBalance = totalGnosisXdaiSum; + gnosisVaultRegBalance = totalGnosisVaultRegSum; + // Stocker les détails par wallet + perWalletBalances = walletDetails; - // Archivage global si une heure s'est écoulée depuis le dernier archivage - if (lastArchiveTime == null || DateTime.now().difference(lastArchiveTime!).inMinutes >= 5) { - if (timestamp != null) { - _archiveManager.archiveBalance('usdcDeposit', totalUsdcDepositSum, timestamp); - _archiveManager.archiveBalance('usdcBorrow', totalUsdcBorrowSum, timestamp); - _archiveManager.archiveBalance('xdaiDeposit', totalXdaiDepositSum, timestamp); - _archiveManager.archiveBalance('xdaiBorrow', totalXdaiBorrowSum, timestamp); - lastArchiveTime = DateTime.now(); + // Calcul de l'APY GLOBAL uniquement après avoir accumulé les totaux + try { + usdcDepositApy = await calculateAPY('usdcDeposit'); + usdcBorrowApy = await calculateAPY('usdcBorrow'); + xdaiDepositApy = await calculateAPY('xdaiDeposit'); + xdaiBorrowApy = await calculateAPY('xdaiBorrow'); + } catch (e) { + debugPrint('Erreur lors du calcul de l\'APY global: $e'); + } + + notifyListeners(); // Notifier l'interface que les données ont été mises à jour + + // Archivage global si une heure s'est écoulée depuis le dernier archivage + if (lastArchiveTime == null || + DateTime.now().difference(lastArchiveTime!).inMinutes >= 5) { + if (timestamp != null) { + _archiveManager.archiveBalance( + 'usdcDeposit', totalUsdcDepositSum, timestamp); + _archiveManager.archiveBalance( + 'usdcBorrow', totalUsdcBorrowSum, timestamp); + _archiveManager.archiveBalance( + 'xdaiDeposit', totalXdaiDepositSum, timestamp); + _archiveManager.archiveBalance( + 'xdaiBorrow', totalXdaiBorrowSum, timestamp); + lastArchiveTime = DateTime.now(); + } + } else { + final timeUntilNextArchive = + Duration(minutes: 5) - DateTime.now().difference(lastArchiveTime!); + final minutesRemaining = timeUntilNextArchive.inMinutes; + final secondsRemaining = timeUntilNextArchive.inSeconds % 60; } - } else { - final timeUntilNextArchive = Duration(minutes: 5) - DateTime.now().difference(lastArchiveTime!); - final minutesRemaining = timeUntilNextArchive.inMinutes; - final secondsRemaining = timeUntilNextArchive.inSeconds % 60; + } catch (e) { + debugPrint('Erreur lors de la récupération des balances RMM: $e'); } - } catch (e) { - debugPrint('Erreur lors de la récupération des balances RMM: $e'); } -} Future calculateAPY(String tokenType) async { // Récupérer l'historique des balances - List history = await _archiveManager.getBalanceHistory(tokenType); + List history = + await _archiveManager.getBalanceHistory(tokenType); // Vérifier s'il y a au moins deux enregistrements pour calculer l'APY if (history.length < 2) { @@ -2329,8 +2427,9 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } // Calculer l'APY moyen global sur toutes les paires en utilisant la méthode exponentielle - double globalApy = apyManager.calculateExponentialMovingAverageAPY(history); - + double globalApy = + apyManager.calculateExponentialMovingAverageAPY(history); + // Vérifier si le résultat global est NaN if (!globalApy.isNaN) { apyAverage = globalApy; @@ -2343,7 +2442,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } } - double getTotalRentReceived() { + double getTotalRentReceived() { return rentData.fold( 0.0, (total, rentEntry) => @@ -2450,9 +2549,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa List> allOffersList = []; if (yamMarketFetched.isNotEmpty) { - for (var offer in yamMarketFetched) { - // Vérifier si le token de l'offre correspond à un token de allTokens final matchingToken = allTokens.firstWhere( (token) => @@ -2500,7 +2597,6 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa 'timsync': offer['timsync'], 'buyHolderAddress': offer['buy_holder_address'], }); - } yamMarket = allOffersList; @@ -2561,55 +2657,56 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa netGlobalApy = calculateGlobalApy(); // Calculer l'APY moyen pondéré par les montants - double totalDepositAmount = totalUsdcDepositBalance + totalXdaiDepositBalance; + double totalDepositAmount = + totalUsdcDepositBalance + totalXdaiDepositBalance; double totalBorrowAmount = totalUsdcBorrowBalance + totalXdaiBorrowBalance; - + // APY pondéré pour les dépôts (gains) - toujours positif double weightedDepositApy = 0.0; if (totalDepositAmount > 0) { - weightedDepositApy = (usdcDepositApy * totalUsdcDepositBalance + - xdaiDepositApy * totalXdaiDepositBalance) / - totalDepositAmount; + weightedDepositApy = (usdcDepositApy * totalUsdcDepositBalance + + xdaiDepositApy * totalXdaiDepositBalance) / + totalDepositAmount; } - + // APY pondéré pour les emprunts (coûts) - toujours positif double weightedBorrowApy = 0.0; if (totalBorrowAmount > 0) { - weightedBorrowApy = (usdcBorrowApy * totalUsdcBorrowBalance + - xdaiBorrowApy * totalXdaiBorrowBalance) / - totalBorrowAmount; + weightedBorrowApy = (usdcBorrowApy * totalUsdcBorrowBalance + + xdaiBorrowApy * totalXdaiBorrowBalance) / + totalBorrowAmount; } - + // Calcul du total des intérêts gagnés et payés double depositInterest = weightedDepositApy * totalDepositAmount; double borrowInterest = weightedBorrowApy * totalBorrowAmount; - + // Intérêt net (positif si les coûts d'emprunt sont supérieurs aux gains de dépôt, // négatif si les gains de dépôt sont supérieurs aux coûts d'emprunt) double netInterest = borrowInterest - depositInterest; - + // Total des montants impliqués double totalAmount = totalDepositAmount + totalBorrowAmount; - + // Calculer l'APY moyen pondéré final if (totalAmount > 0) { apyAverage = netInterest / totalAmount; } else { apyAverage = 0.0; } - + // Vérifier si le résultat est NaN if (apyAverage.isNaN) { apyAverage = 0.0; } - + // Archiver l'APY global calculé _archiveApyValue(netGlobalApy, apyAverage); - // Calculer l'APY pour chaque wallet individuel - Map walletApys = apyManager.calculateWalletApys(walletStats); - + Map walletApys = + apyManager.calculateWalletApys(walletStats); + // Mettre à jour les statistiques de wallet avec les APY calculés for (var wallet in walletStats) { final String address = wallet['address'] as String; @@ -2617,9 +2714,8 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } } - /// Ajuste la réactivité du calcul d'APY - /// + /// /// [reactivityLevel] : niveau de réactivité entre 0 (très lisse) et 1 (très réactif) /// [historyDays] : nombre de jours d'historique à prendre en compte (optionnel) void adjustApyReactivity(double reactivityLevel, {int? historyDays}) { @@ -2631,19 +2727,20 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Une réactivité de 0 donne un alpha de 0.05 (très lisse) // Une réactivité de 1 donne un alpha de 0.8 (très réactif) double alpha = 0.05 + (reactivityLevel * 0.75); - + // Déterminer le nombre de jours d'historique // Si non spécifié, ajuster en fonction de la réactivité // Plus la réactivité est élevée, moins on a besoin d'historique // Plage de 1 à 20 jours avec des valeurs discrètes - int days = historyDays ?? (20 - (reactivityLevel * 19).round()).clamp(1, 20); - + int days = + historyDays ?? (20 - (reactivityLevel * 19).round()).clamp(1, 20); + // Appliquer les nouveaux paramètres à l'ApyManager apyManager.setApyCalculationParameters( newEmaAlpha: alpha, newMaxHistoryDays: days, ); - + // Recalculer l'APY avec les nouveaux paramètres if (balanceHistory.length >= 2) { try { @@ -2652,14 +2749,14 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa debugPrint("❌ Erreur lors du recalcul de l'APY: $e"); } } else { - debugPrint("⚠️ Historique insuffisant pour recalculer l'APY: ${balanceHistory.length} enregistrement(s) disponible(s) (minimum requis: 2)"); + debugPrint( + "⚠️ Historique insuffisant pour recalculer l'APY: ${balanceHistory.length} enregistrement(s) disponible(s) (minimum requis: 2)"); } - + // Notifier les widgets pour qu'ils se mettent à jour notifyListeners(); } - void _archiveApyValue(double netApy, double grossApy) { debugPrint('🔍 _archiveApyValue: netApy: $netApy, grossApy: $grossApy'); // Vérifier si nous avons moins de 20 éléments dans l'historique @@ -2667,9 +2764,11 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Si moins de 20 éléments, vérifier si 15 minutes se sont écoulées depuis le dernier archivage if (apyHistory.isNotEmpty) { final lastRecord = apyHistory.last; - final timeSinceLastRecord = DateTime.now().difference(lastRecord.timestamp); + final timeSinceLastRecord = + DateTime.now().difference(lastRecord.timestamp); if (timeSinceLastRecord.inMinutes < 15) { - debugPrint('⏳ Archivage APY ignoré: moins de 15 minutes depuis le dernier enregistrement (${timeSinceLastRecord.inMinutes}m)'); + debugPrint( + '⏳ Archivage APY ignoré: moins de 15 minutes depuis le dernier enregistrement (${timeSinceLastRecord.inMinutes}m)'); return; } } @@ -2677,23 +2776,22 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa // Si 20 éléments ou plus, vérifier si 1 heure s'est écoulée depuis le dernier archivage if (apyHistory.isNotEmpty) { final lastRecord = apyHistory.last; - final timeSinceLastRecord = DateTime.now().difference(lastRecord.timestamp); + final timeSinceLastRecord = + DateTime.now().difference(lastRecord.timestamp); if (timeSinceLastRecord.inHours < 1) { - debugPrint('⏳ Archivage APY ignoré: moins d\'une heure depuis le dernier enregistrement (${timeSinceLastRecord.inMinutes}m)'); + debugPrint( + '⏳ Archivage APY ignoré: moins d\'une heure depuis le dernier enregistrement (${timeSinceLastRecord.inMinutes}m)'); return; } } } // 1. Ajouter à la liste en mémoire - apyHistory.add(APYRecord( - apy: netApy, - timestamp: DateTime.now()) - ); - + apyHistory.add(APYRecord(apy: netApy, timestamp: DateTime.now())); + // 2. Déléguer à l'ArchiveManager pour la persistance dans Hive _archiveManager.archiveApyValue(netApy, grossApy); - + // 3. Notifier les widgets pour mise à jour de l'UI notifyListeners(); } @@ -2715,10 +2813,14 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa Future updateApyValues() async { try { // Calculer l'APY à partir de l'historique pour chaque type de balance - usdcDepositApy = apyManager.calculateSmartAPY(await _archiveManager.getBalanceHistory('usdcDeposit')); - usdcBorrowApy = apyManager.calculateSmartAPY(await _archiveManager.getBalanceHistory('usdcBorrow')); - xdaiDepositApy = apyManager.calculateSmartAPY(await _archiveManager.getBalanceHistory('xdaiDeposit')); - xdaiBorrowApy = apyManager.calculateSmartAPY(await _archiveManager.getBalanceHistory('xdaiBorrow')); + usdcDepositApy = apyManager.calculateSmartAPY( + await _archiveManager.getBalanceHistory('usdcDeposit')); + usdcBorrowApy = apyManager.calculateSmartAPY( + await _archiveManager.getBalanceHistory('usdcBorrow')); + xdaiDepositApy = apyManager.calculateSmartAPY( + await _archiveManager.getBalanceHistory('xdaiDeposit')); + xdaiBorrowApy = apyManager.calculateSmartAPY( + await _archiveManager.getBalanceHistory('xdaiBorrow')); // Vérifier et corriger les valeurs NaN if (usdcDepositApy.isNaN) usdcDepositApy = 0.0; @@ -2741,28 +2843,37 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa double calculateGlobalApy() { try { // Calculer le dénominateur - double denominator = walletValue + rmmValue + totalUsdcDepositBalance + totalXdaiDepositBalance + totalUsdcBorrowBalance + totalXdaiBorrowBalance; - + double denominator = walletValue + + rmmValue + + totalUsdcDepositBalance + + totalXdaiDepositBalance + + totalUsdcBorrowBalance + + totalXdaiBorrowBalance; + // Si le dénominateur est très proche de 0, retourner 0 if (denominator < 0.000001) { - debugPrint("⚠️ Le dénominateur est trop proche de 0 pour calculer l'APY global"); + debugPrint( + "⚠️ Le dénominateur est trop proche de 0 pour calculer l'APY global"); return 0.0; } // Calculer le numérateur double numerator = (averageAnnualYield * (walletValue + rmmValue)) + - (totalUsdcDepositBalance * usdcDepositApy + totalXdaiDepositBalance * xdaiDepositApy) - - (totalUsdcBorrowBalance * usdcBorrowApy + totalXdaiBorrowBalance * xdaiBorrowApy); + (totalUsdcDepositBalance * usdcDepositApy + + totalXdaiDepositBalance * xdaiDepositApy) - + (totalUsdcBorrowBalance * usdcBorrowApy + + totalXdaiBorrowBalance * xdaiBorrowApy); // Calculer le résultat double result = numerator / denominator; - + // Vérifier si le résultat est NaN, infini ou trop grand if (result.isNaN || result.isInfinite || result.abs() > 3650) { - debugPrint("⚠️ L'APY global calculé est ${result.isNaN ? "NaN" : result.isInfinite ? "infini" : "trop grand"}, retourne 0.0"); + debugPrint( + "⚠️ L'APY global calculé est ${result.isNaN ? "NaN" : result.isInfinite ? "infini" : "trop grand"}, retourne 0.0"); return 0.0; } - + return result; } catch (e) { debugPrint("❌ Erreur lors du calcul de l'APY global: $e"); @@ -2774,12 +2885,13 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa List> getRentHistoryForToken(String token) { token = token.toLowerCase(); List> history = []; - + // Parcourir l'historique des loyers par date for (var dateEntry in rentHistory) { String date = dateEntry['date']; - List> rents = List>.from(dateEntry['rents']); - + List> rents = + List>.from(dateEntry['rents']); + // Rechercher ce token dans les loyers de cette date for (var rentEntry in rents) { if (rentEntry['token'].toLowerCase() == token) { @@ -2793,10 +2905,10 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } } } - + return history; } - + // Méthode pour obtenir tous les loyers cumulés (déjà disponible via cumulativeRentsByToken) Map getAllCumulativeRents() { return Map.from(cumulativeRentsByToken); @@ -2811,24 +2923,28 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa Map> getRentsByWallet() { return Map>.from(cumulativeRentsByWallet); } - + /// Méthode centralisée pour calculer l'APY seulement si toutes les données nécessaires sont disponibles /// Cette méthode devrait être appelée après le chargement des données importantes bool safeCalculateApyValues() { // Vérifier que nous avons suffisamment de données pour calculer l'APY if (balanceHistory.length < 2) { - debugPrint("⚠️ Historique insuffisant pour calculer l'APY: ${balanceHistory.length} enregistrement(s) (minimum requis: 2)"); + debugPrint( + "⚠️ Historique insuffisant pour calculer l'APY: ${balanceHistory.length} enregistrement(s) (minimum requis: 2)"); return false; } - + // Vérifier que les données financières essentielles sont disponibles - if (totalUsdcDepositBalance == 0.0 && totalXdaiDepositBalance == 0.0 && - totalUsdcBorrowBalance == 0.0 && totalXdaiBorrowBalance == 0.0 && - walletValue == 0.0 && rmmValue == 0.0) { + if (totalUsdcDepositBalance == 0.0 && + totalXdaiDepositBalance == 0.0 && + totalUsdcBorrowBalance == 0.0 && + totalXdaiBorrowBalance == 0.0 && + walletValue == 0.0 && + rmmValue == 0.0) { debugPrint("⚠️ Données financières insuffisantes pour calculer l'APY"); return false; } - + try { // Calculer l'APY à partir des données disponibles calculateApyValues(); @@ -2862,7 +2978,8 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa Future saveRoiHistory() async { try { var box = Hive.box('roiValueArchive'); - List> roiHistoryJson = roiHistory.map((record) => record.toJson()).toList(); + List> roiHistoryJson = + roiHistory.map((record) => record.toJson()).toList(); await box.put('roi_history', roiHistoryJson); await box.flush(); // Forcer l'écriture sur le disque debugPrint("✅ Historique ROI sauvegardé avec succès."); @@ -2875,7 +2992,8 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa Future saveApyHistory() async { try { var box = Hive.box('apyValueArchive'); - List> apyHistoryJson = apyHistory.map((record) => record.toJson()).toList(); + List> apyHistoryJson = + apyHistory.map((record) => record.toJson()).toList(); await box.put('apy_history', apyHistoryJson); notifyListeners(); } catch (e) { @@ -2886,21 +3004,27 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa /// Diagnostique l'état du cache des wallets pour identifier les problèmes de données Future> diagnoseCacheStatus() async { try { - debugPrint("$_logTask Lancement du diagnostic du cache pour ${evmAddresses.length} wallets"); - + debugPrint( + "$_logTask Lancement du diagnostic du cache pour ${evmAddresses.length} wallets"); + final diagnostics = await ApiService.diagnoseCacheStatus(evmAddresses); - + // Log des résultats principaux final globalCache = diagnostics['globalCacheStatus']; - debugPrint("$_logDetail Cache global rent: ${globalCache['cachedRentData']}"); - debugPrint("$_logDetail Cache global detailed: ${globalCache['cachedDetailedRentDataAll']}"); - debugPrint("$_logDetail Dernière erreur 429 rent: ${globalCache['lastRent429Time']}"); - debugPrint("$_logDetail Dernière erreur 429 detailed: ${globalCache['lastDetailedRent429Time']}"); - - final walletDiagnostics = diagnostics['walletDiagnostics'] as Map; + debugPrint( + "$_logDetail Cache global rent: ${globalCache['cachedRentData']}"); + debugPrint( + "$_logDetail Cache global detailed: ${globalCache['cachedDetailedRentDataAll']}"); + debugPrint( + "$_logDetail Dernière erreur 429 rent: ${globalCache['lastRent429Time']}"); + debugPrint( + "$_logDetail Dernière erreur 429 detailed: ${globalCache['lastDetailedRent429Time']}"); + + final walletDiagnostics = + diagnostics['walletDiagnostics'] as Map; int walletsWithRentCache = 0; int walletsWithDetailedCache = 0; - + for (String wallet in evmAddresses) { final walletInfo = walletDiagnostics[wallet]; if (walletInfo != null && walletInfo['rentCacheExists'] == true) { @@ -2910,13 +3034,14 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa walletsWithDetailedCache++; } } - - debugPrint("$_logDetail Wallets avec cache rent: $walletsWithRentCache/${evmAddresses.length}"); - debugPrint("$_logDetail Wallets avec cache detailed: $walletsWithDetailedCache/${evmAddresses.length}"); - + + debugPrint( + "$_logDetail Wallets avec cache rent: $walletsWithRentCache/${evmAddresses.length}"); + debugPrint( + "$_logDetail Wallets avec cache detailed: $walletsWithDetailedCache/${evmAddresses.length}"); + debugPrint("$_logSuccess Diagnostic du cache terminé"); return diagnostics; - } catch (e) { debugPrint("$_logError Erreur lors du diagnostic du cache: $e"); return { @@ -2930,16 +3055,16 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa void processTokenHistory() { final startTime = DateTime.now(); debugPrint("$_logSub Traitement de l'historique des tokens..."); - + if (tokenHistoryData.isEmpty) { debugPrint("$_logWarning Aucune donnée d'historique de token disponible"); return; } - + try { // Grouper l'historique par token_uuid Map>> historyByToken = {}; - + for (var historyEntry in tokenHistoryData) { String tokenUuid = historyEntry['token_uuid']?.toLowerCase() ?? ''; if (tokenUuid.isNotEmpty) { @@ -2949,7 +3074,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa historyByToken[tokenUuid]!.add(historyEntry); } } - + // Trier l'historique de chaque token par date (du plus récent au plus ancien) historyByToken.forEach((tokenUuid, history) { history.sort((a, b) { @@ -2958,7 +3083,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa return dateB.compareTo(dateA); // Tri décroissant }); }); - + // Associer l'historique aux tokens dans allTokens for (var token in _allTokens) { String tokenUuid = token['uuid']?.toLowerCase() ?? ''; @@ -2968,7 +3093,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa token['history'] = >[]; } } - + // Associer l'historique aux tokens dans portfolio for (var token in _portfolio) { String tokenUuid = token['uuid']?.toLowerCase() ?? ''; @@ -2978,20 +3103,22 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa token['history'] = >[]; } } - + final duration = DateTime.now().difference(startTime); - debugPrint("$_logSuccess Historique des tokens traité: ${historyByToken.length} tokens avec historique (${duration.inMilliseconds}ms)"); + debugPrint( + "$_logSuccess Historique des tokens traité: ${historyByToken.length} tokens avec historique (${duration.inMilliseconds}ms)"); } catch (e) { - debugPrint("$_logError Erreur lors du traitement de l'historique des tokens: $e"); + debugPrint( + "$_logError Erreur lors du traitement de l'historique des tokens: $e"); } } /// Méthode pour obtenir l'historique d'un token spécifique List> getTokenHistory(String tokenUuid) { tokenUuid = tokenUuid.toLowerCase(); - return tokenHistoryData.where((entry) => - entry['token_uuid']?.toLowerCase() == tokenUuid - ).toList() + return tokenHistoryData + .where((entry) => entry['token_uuid']?.toLowerCase() == tokenUuid) + .toList() ..sort((a, b) { String dateA = a['date'] ?? ''; String dateB = b['date'] ?? ''; @@ -3000,19 +3127,21 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } /// Méthode pour obtenir les modifications récentes (derniers 30 jours) - List> getRecentTokenChanges({int? days = 365, bool includeAllChanges = false}) { + List> getRecentTokenChanges( + {int? days = 365, bool includeAllChanges = false}) { // Si days est null, pas de filtre de date (tous les changements) - final DateTime? cutoffDate = days != null ? DateTime.now().subtract(Duration(days: days)) : null; - + final DateTime? cutoffDate = + days != null ? DateTime.now().subtract(Duration(days: days)) : null; + List> recentChanges = []; - + // Grouper par token pour détecter les changements Map>> historyByToken = {}; - + for (var entry in tokenHistoryData) { String tokenUuid = entry['token_uuid']?.toLowerCase() ?? ''; String dateStr = entry['date'] ?? ''; - + if (tokenUuid.isNotEmpty && dateStr.isNotEmpty) { try { DateTime entryDate = DateTime.parse(dateStr); @@ -3028,44 +3157,43 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } } } - + // Pour chaque token, détecter les changements entre les entrées historyByToken.forEach((tokenUuid, history) { // Trier par date - history.sort((a, b) => DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); - + history.sort((a, b) => + DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); + for (int i = 1; i < history.length; i++) { var previous = history[i - 1]; var current = history[i]; - + // Détecter les changements dans les champs importants - List> changes = _detectChanges(previous, current, tokenUuid, includeAllChanges); + List> changes = + _detectChanges(previous, current, tokenUuid, includeAllChanges); recentChanges.addAll(changes); } }); - + // Trier les changements par date (du plus récent au plus ancien) - recentChanges.sort((a, b) => DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); - + recentChanges.sort((a, b) => + DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); + return recentChanges; } /// Détecte les changements entre deux entrées d'historique - List> _detectChanges( - Map previous, - Map current, - String tokenUuid, - bool includeAllChanges - ) { + List> _detectChanges(Map previous, + Map current, String tokenUuid, bool includeAllChanges) { List> changes = []; - + // Champs liés aux loyers (toujours affichés) final rentFields = { 'gross_rent_year': 'Loyer brut annuel', 'net_rent_year': 'Loyer net annuel', 'rented_units': 'Unités louées', }; - + // Autres champs (affichés seulement si includeAllChanges = true) final otherFields = { 'token_price': 'Prix du token', @@ -3074,19 +3202,19 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa 'initial_maintenance_reserve': 'Réserve de maintenance initiale', 'renovation_reserve': 'Réserve de rénovation', }; - + // Combiner les champs selon le paramètre Map fieldsToWatch = Map.from(rentFields); if (includeAllChanges) { fieldsToWatch.addAll(otherFields); } - + // Trouver le token correspondant pour obtenir les informations d'affichage Map tokenInfo = _allTokens.firstWhere( (token) => token['uuid']?.toLowerCase() == tokenUuid, orElse: () => {'shortName': 'Token inconnu', 'imageLink': ''}, ); - + // Gérer le cas où imageLink peut être une liste String imageLink = ''; var imageData = tokenInfo['imageLink']; @@ -3096,11 +3224,11 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa imageLink = imageData; } tokenInfo['imageLink'] = imageLink; - + fieldsToWatch.forEach((field, label) { var prevValue = previous[field]; var currValue = current[field]; - + if (prevValue != null && currValue != null && prevValue != currValue) { changes.add({ 'token_uuid': tokenUuid, @@ -3115,7 +3243,7 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa }); } }); - + return changes; } @@ -3130,4 +3258,4 @@ debugPrint("🗃️ Début récupération et calcul des données pour le Dashboa } return 'change'; } -} \ No newline at end of file +} diff --git a/lib/modals/agenda.dart b/lib/modals/agenda.dart index 517efc5..a6798ef 100644 --- a/lib/modals/agenda.dart +++ b/lib/modals/agenda.dart @@ -32,18 +32,18 @@ class AgendaCalendarState extends State { final dataManager = Provider.of(context, listen: false); _events = _extractTransactions(widget.portfolio); _addRentEvents(_events, dataManager.rentData); - + // Ajout d'un listener pour le défilement _scrollController.addListener(_onScroll); } - + @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } - + void _onScroll() { // On réduit le calendrier si on scrolle vers le bas if (_scrollController.offset > 20 && !_isCalendarCollapsed) { @@ -136,29 +136,27 @@ class AgendaCalendarState extends State { curve: Curves.easeInOut, height: _isCalendarCollapsed ? 80 : null, decoration: BoxDecoration( - color: isDarkMode - ? CupertinoColors.systemGrey6.darkColor + color: isDarkMode + ? CupertinoColors.systemGrey6.darkColor : CupertinoColors.systemGrey6.color, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), padding: EdgeInsets.symmetric( - vertical: _isCalendarCollapsed ? 8 : 12, - horizontal: 8 - ), - child: _isCalendarCollapsed - ? _buildCollapsedCalendar() + vertical: _isCalendarCollapsed ? 8 : 12, horizontal: 8), + child: _isCalendarCollapsed + ? _buildCollapsedCalendar() : _buildFullCalendar(isDarkMode, context), ), - + const SizedBox(height: 16), - + // Titre de section avec bouton pour contrôler le calendrier Padding( padding: const EdgeInsets.only(left: 8, bottom: 8), @@ -175,18 +173,18 @@ class AgendaCalendarState extends State { ), CupertinoButton( padding: EdgeInsets.zero, + onPressed: _toggleCalendarSize, child: Icon( - _isCalendarCollapsed - ? CupertinoIcons.calendar_badge_plus + _isCalendarCollapsed + ? CupertinoIcons.calendar_badge_plus : CupertinoIcons.calendar_badge_minus, color: CupertinoColors.systemBlue.resolveFrom(context), ), - onPressed: _toggleCalendarSize, ), ], ), ), - + Expanded( child: (_events.containsKey(_selectedDay) && _events[_selectedDay]!.isNotEmpty) @@ -199,7 +197,8 @@ class AgendaCalendarState extends State { separatorBuilder: (context, index) => Divider( height: 1, indent: 70, - color: CupertinoColors.systemGrey5.resolveFrom(context), + color: + CupertinoColors.systemGrey5.resolveFrom(context), ), itemBuilder: (context, index) { final event = _events[_selectedDay]![index]; @@ -214,15 +213,18 @@ class AgendaCalendarState extends State { event.containsKey('transactionType') ? event['transactionType'] : S.of(context).unknownTransaction; - + // Obtenir la version localisée du type de transaction pour l'affichage - final localizedTransactionType = _getLocalizedTransactionType(transactionType, context); + final localizedTransactionType = + _getLocalizedTransactionType( + transactionType, context); return Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), decoration: BoxDecoration( - color: isDarkMode - ? CupertinoColors.systemGrey6.darkColor + color: isDarkMode + ? CupertinoColors.systemGrey6.darkColor : CupertinoColors.systemGrey6.color, borderRadius: BorderRadius.circular(0), ), @@ -233,12 +235,15 @@ class AgendaCalendarState extends State { width: 48, height: 48, decoration: BoxDecoration( - color: _getIconBackground(transactionType, context), - borderRadius: BorderRadius.circular(12), + color: _getIconBackground( + transactionType, context), + borderRadius: + BorderRadius.circular(12), ), child: Center( child: Icon( - _getTransactionIcon(transactionType), + _getTransactionIcon( + transactionType), color: Colors.white, size: 24, ), @@ -247,30 +252,42 @@ class AgendaCalendarState extends State { const SizedBox(width: 12), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( fullName, style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), + fontSize: 16 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w600, - color: CupertinoColors.label.resolveFrom(context), + color: CupertinoColors.label + .resolveFrom(context), ), ), const SizedBox(height: 4), Text( "$localizedTransactionType • ${S.of(context).quantity}: $amount", style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), - color: CupertinoColors.secondaryLabel.resolveFrom(context), + fontSize: 14 + + appState + .getTextSizeOffset(), + color: CupertinoColors + .secondaryLabel + .resolveFrom(context), ), ), Text( price, style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: 14 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w500, - color: CupertinoColors.systemBlue.resolveFrom(context), + color: CupertinoColors + .systemBlue + .resolveFrom(context), ), ), ], @@ -284,8 +301,10 @@ class AgendaCalendarState extends State { width: 48, height: 48, decoration: BoxDecoration( - color: CupertinoColors.systemGreen.resolveFrom(context), - borderRadius: BorderRadius.circular(12), + color: CupertinoColors.systemGreen + .resolveFrom(context), + borderRadius: + BorderRadius.circular(12), ), child: const Center( child: Icon( @@ -298,22 +317,31 @@ class AgendaCalendarState extends State { const SizedBox(width: 12), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).wallet, style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), - color: CupertinoColors.secondaryLabel.resolveFrom(context), + fontSize: 16 + + appState + .getTextSizeOffset(), + color: CupertinoColors + .secondaryLabel + .resolveFrom(context), ), ), const SizedBox(height: 4), Text( "${currencyUtils.convert(amount).toStringAsFixed(2)} ${currencyUtils.currencySymbol}", style: TextStyle( - fontSize: 15 + appState.getTextSizeOffset(), + fontSize: 15 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w500, - color: CupertinoColors.systemGreen.resolveFrom(context), + color: CupertinoColors + .systemGreen + .resolveFrom(context), ), ), ], @@ -329,8 +357,8 @@ class AgendaCalendarState extends State { child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: isDarkMode - ? CupertinoColors.systemGrey6.darkColor + color: isDarkMode + ? CupertinoColors.systemGrey6.darkColor : CupertinoColors.systemGrey6.color, borderRadius: BorderRadius.circular(16), ), @@ -340,14 +368,16 @@ class AgendaCalendarState extends State { Icon( CupertinoIcons.calendar_badge_minus, size: 50, - color: CupertinoColors.systemGrey.resolveFrom(context), + color: CupertinoColors.systemGrey + .resolveFrom(context), ), const SizedBox(height: 12), Text( S.of(context).wallet, style: TextStyle( fontSize: 16 + appState.getTextSizeOffset(), - color: CupertinoColors.secondaryLabel.resolveFrom(context), + color: CupertinoColors.secondaryLabel + .resolveFrom(context), ), ), ], @@ -360,7 +390,7 @@ class AgendaCalendarState extends State { ), ); } - + // Version compacte du calendrier Widget _buildCollapsedCalendar() { return GestureDetector( @@ -381,7 +411,9 @@ class AgendaCalendarState extends State { Text( "${_selectedDay.day}/${_selectedDay.month}/${_selectedDay.year}", style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.label.resolveFrom(context), ), @@ -390,7 +422,9 @@ class AgendaCalendarState extends State { Text( _getFormattedDay(_selectedDay), style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), @@ -410,7 +444,8 @@ class AgendaCalendarState extends State { ), onPressed: () { setState(() { - _selectedDay = _selectedDay.subtract(const Duration(days: 1)); + _selectedDay = + _selectedDay.subtract(const Duration(days: 1)); _focusedDay = _selectedDay; }); }, @@ -437,7 +472,7 @@ class AgendaCalendarState extends State { ), ); } - + // Version complète du calendrier Widget _buildFullCalendar(bool isDarkMode, BuildContext context) { return TableCalendar( @@ -451,8 +486,8 @@ class AgendaCalendarState extends State { }, onDaySelected: (selectedDay, focusedDay) { setState(() { - _selectedDay = DateTime( - selectedDay.year, selectedDay.month, selectedDay.day); + _selectedDay = + DateTime(selectedDay.year, selectedDay.month, selectedDay.day); _focusedDay = focusedDay; }); }, @@ -461,18 +496,13 @@ class AgendaCalendarState extends State { headerStyle: HeaderStyle( titleCentered: true, formatButtonVisible: false, - leftChevronIcon: Icon( - CupertinoIcons.chevron_left, - size: 16, - color: CupertinoColors.systemBlue.resolveFrom(context) - ), - rightChevronIcon: Icon( - CupertinoIcons.chevron_right, - size: 16, - color: CupertinoColors.systemBlue.resolveFrom(context) - ), + leftChevronIcon: Icon(CupertinoIcons.chevron_left, + size: 16, color: CupertinoColors.systemBlue.resolveFrom(context)), + rightChevronIcon: Icon(CupertinoIcons.chevron_right, + size: 16, color: CupertinoColors.systemBlue.resolveFrom(context)), titleTextStyle: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.label.resolveFrom(context), ), @@ -488,7 +518,9 @@ class AgendaCalendarState extends State { markersAlignment: Alignment.bottomCenter, markersAnchor: 0.7, todayDecoration: BoxDecoration( - color: CupertinoColors.systemBlue.resolveFrom(context).withOpacity(0.3), + color: CupertinoColors.systemBlue + .resolveFrom(context) + .withValues(alpha: 0.3), shape: BoxShape.circle, ), todayTextStyle: TextStyle( @@ -507,12 +539,14 @@ class AgendaCalendarState extends State { ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false).getTextSizeOffset(), color: CupertinoColors.secondaryLabel.resolveFrom(context), fontWeight: FontWeight.w500, ), weekendStyle: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false).getTextSizeOffset(), color: CupertinoColors.systemGrey.resolveFrom(context), fontWeight: FontWeight.w500, ), @@ -522,8 +556,8 @@ class AgendaCalendarState extends State { if (events.isEmpty) return null; List markers = []; - bool hasRent = events - .any((e) => (e as Map)['type'] == 'rent'); + bool hasRent = + events.any((e) => (e as Map)['type'] == 'rent'); bool hasPurchase = events.any((e) => (e as Map)['type'] == 'transaction' && e['transactionType'] == DataManager.transactionTypePurchase); @@ -540,7 +574,7 @@ class AgendaCalendarState extends State { width: 6, height: 6, decoration: BoxDecoration( - color: CupertinoColors.systemGreen.resolveFrom(context), + color: CupertinoColors.systemGreen.resolveFrom(context), shape: BoxShape.circle))); } if (hasPurchase) { @@ -549,7 +583,7 @@ class AgendaCalendarState extends State { width: 6, height: 6, decoration: BoxDecoration( - color: CupertinoColors.systemBlue.resolveFrom(context), + color: CupertinoColors.systemBlue.resolveFrom(context), shape: BoxShape.circle))); } if (hasYAM) { @@ -558,7 +592,7 @@ class AgendaCalendarState extends State { width: 6, height: 6, decoration: BoxDecoration( - color: CupertinoColors.systemOrange.resolveFrom(context), + color: CupertinoColors.systemOrange.resolveFrom(context), shape: BoxShape.circle))); } if (hasTransfer) { @@ -567,7 +601,7 @@ class AgendaCalendarState extends State { width: 6, height: 6, decoration: BoxDecoration( - color: CupertinoColors.systemGrey.resolveFrom(context), + color: CupertinoColors.systemGrey.resolveFrom(context), shape: BoxShape.circle))); } @@ -580,10 +614,18 @@ class AgendaCalendarState extends State { ), ); } - + // Fonction pour obtenir le jour de la semaine formaté String _getFormattedDay(DateTime date) { - const days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']; + const days = [ + 'Lundi', + 'Mardi', + 'Mercredi', + 'Jeudi', + 'Vendredi', + 'Samedi', + 'Dimanche' + ]; // Le weekday de DateTime va de 1 (lundi) à 7 (dimanche) final dayIndex = date.weekday - 1; return days[dayIndex]; @@ -612,9 +654,10 @@ class AgendaCalendarState extends State { return CupertinoColors.systemTeal.resolveFrom(context); } } - + // Méthode pour traduire les constantes en textes localisés - String _getLocalizedTransactionType(String transactionType, BuildContext context) { + String _getLocalizedTransactionType( + String transactionType, BuildContext context) { if (transactionType == DataManager.transactionTypePurchase) { return S.of(context).purchase; } else if (transactionType == DataManager.transactionTypeTransfer) { diff --git a/lib/modals/modal_others_pie.dart b/lib/modals/modal_others_pie.dart index bdf843a..d200932 100644 --- a/lib/modals/modal_others_pie.dart +++ b/lib/modals/modal_others_pie.dart @@ -5,7 +5,8 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:provider/provider.dart'; -void showOtherDetailsModal(BuildContext context, dataManager, List> othersDetails, String key) { +void showOtherDetailsModal(BuildContext context, dataManager, + List> othersDetails, String key) { // S'assurer que la liste n'est pas vide pour éviter les erreurs de rendu if (othersDetails.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -49,7 +50,9 @@ void showOtherDetailsModal(BuildContext context, dataManager, List(context, listen: false).getTextSizeOffset(), + fontSize: 22 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: CupertinoColors.label, ), @@ -67,15 +70,24 @@ void showOtherDetailsModal(BuildContext context, dataManager, List(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.secondaryLabel, ), @@ -113,8 +128,10 @@ void showOtherDetailsModal(BuildContext context, dataManager, List( valueListenable: selectedIndexNotifier, builder: (context, selectedIndex, _) { - return _buildLegendGrid(othersDetails, key, selectedIndex, (index) { - selectedIndexNotifier.value = selectedIndex == index ? null : index; + return _buildLegendGrid( + othersDetails, key, selectedIndex, (index) { + selectedIndexNotifier.value = + selectedIndex == index ? null : index; }); }), ), @@ -132,11 +149,14 @@ void showOtherDetailsModal(BuildContext context, dataManager, List> othersDetails, String key, int? selectedIndex) { +Widget _buildCenterText(BuildContext context, + List> othersDetails, String key, int? selectedIndex) { if (selectedIndex == null) { // Afficher le total quand rien n'est sélectionné final int totalCount = othersDetails.fold(0, (sum, item) { - final count = item['count'] is int ? item['count'] as int : (item['count'] as double).round(); + final count = item['count'] is int + ? item['count'] as int + : (item['count'] as double).round(); return sum + count; }); @@ -146,7 +166,9 @@ Widget _buildCenterText(BuildContext context, List> othersD Text( S.of(context).total, style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: CupertinoColors.label, ), @@ -155,7 +177,9 @@ Widget _buildCenterText(BuildContext context, List> othersD Text( '$totalCount', style: TextStyle( - fontSize: 22 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 22 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.systemGrey, ), @@ -167,7 +191,9 @@ Widget _buildCenterText(BuildContext context, List> othersD if (selectedIndex < othersDetails.length) { final selectedItem = othersDetails[selectedIndex]; final String name = selectedItem[key] ?? S.of(context).unknown; - final count = selectedItem['count'] is int ? selectedItem['count'] as int : (selectedItem['count'] as double).round(); + final count = selectedItem['count'] is int + ? selectedItem['count'] as int + : (selectedItem['count'] as double).round(); return Column( mainAxisSize: MainAxisSize.min, @@ -176,7 +202,9 @@ Widget _buildCenterText(BuildContext context, List> othersD name, textAlign: TextAlign.center, style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: _getColorForIndex(selectedIndex), ), @@ -187,7 +215,9 @@ Widget _buildCenterText(BuildContext context, List> othersD Text( '$count', style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.systemGrey, ), @@ -201,7 +231,8 @@ Widget _buildCenterText(BuildContext context, List> othersD } } -List _buildOtherDetailsDonutData(List> othersDetails, String key, int? selectedIndex) { +List _buildOtherDetailsDonutData( + List> othersDetails, String key, int? selectedIndex) { // Couleurs iOS final List sectionColors = [ CupertinoColors.systemBlue, @@ -221,7 +252,9 @@ List _buildOtherDetailsDonutData(List> // Calculer la valeur totale double totalValue = 0; for (var item in othersDetails) { - double itemValue = (item['count'] is int) ? (item['count'] as int).toDouble() : (item['count'] as double); + double itemValue = (item['count'] is int) + ? (item['count'] as int).toDouble() + : (item['count'] as double); totalValue += itemValue; } @@ -230,7 +263,9 @@ List _buildOtherDetailsDonutData(List> for (int i = 0; i < othersDetails.length; i++) { final item = othersDetails[i]; - final double value = (item['count'] is int) ? (item['count'] as int).toDouble() : (item['count'] as double); + final double value = (item['count'] is int) + ? (item['count'] as int).toDouble() + : (item['count'] as double); final double percentage = (value / totalValue) * 100; final bool isSelected = selectedIndex == i; @@ -240,7 +275,8 @@ List _buildOtherDetailsDonutData(List> sections.add(PieChartSectionData( value: value, title: '${percentage.toStringAsFixed(1)}%', - color: color.withOpacity(selectedIndex != null && !isSelected ? 0.5 : 1.0), + color: color.withValues( + alpha: selectedIndex != null && !isSelected ? 0.5 : 1.0), radius: radius, titleStyle: TextStyle( fontSize: isSelected ? 14 : 12, @@ -256,7 +292,8 @@ List _buildOtherDetailsDonutData(List> return sections; } -Widget _buildLegendGrid(List> othersDetails, String key, int? selectedIndex, Function(int) onTap) { +Widget _buildLegendGrid(List> othersDetails, String key, + int? selectedIndex, Function(int) onTap) { // Couleurs iOS final List sectionColors = [ CupertinoColors.systemBlue, @@ -286,7 +323,9 @@ Widget _buildLegendGrid(List> othersDetails, String key, in itemBuilder: (context, index) { final item = othersDetails[index]; final String name = item[key] ?? 'Inconnu'; - final double value = (item['count'] is int) ? (item['count'] as int).toDouble() : (item['count'] as double); + final double value = (item['count'] is int) + ? (item['count'] as int).toDouble() + : (item['count'] as double); final Color color = sectionColors[index % sectionColors.length]; final bool isSelected = selectedIndex == index; @@ -294,7 +333,9 @@ Widget _buildLegendGrid(List> othersDetails, String key, in onTap: () => onTap(index), child: Container( decoration: BoxDecoration( - color: isSelected ? color.withOpacity(0.1) : CupertinoColors.white, + color: isSelected + ? color.withValues(alpha: 0.1) + : CupertinoColors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? color : Colors.transparent, @@ -302,7 +343,7 @@ Widget _buildLegendGrid(List> othersDetails, String key, in ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withValues(alpha: 0.03), spreadRadius: 0, blurRadius: 2, offset: Offset(0, 1), @@ -325,7 +366,9 @@ Widget _buildLegendGrid(List> othersDetails, String key, in child: Text( name, style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: isSelected ? color : CupertinoColors.label, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, ), @@ -336,7 +379,9 @@ Widget _buildLegendGrid(List> othersDetails, String key, in Text( value.toStringAsFixed(0), style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: isSelected ? color : CupertinoColors.secondaryLabel, ), @@ -362,7 +407,7 @@ Widget _buildSelectedIndicator(Color color) { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 2, offset: const Offset(0, 1), ), diff --git a/lib/modals/token_details/showTokenDetails.dart b/lib/modals/token_details/showTokenDetails.dart index 36401b0..d954d4f 100644 --- a/lib/modals/token_details/showTokenDetails.dart +++ b/lib/modals/token_details/showTokenDetails.dart @@ -15,7 +15,6 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:show_network_image/show_network_image.dart'; import '../../pages/portfolio/FullScreenCarousel.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; -import 'package:autoscale_tabbarview/autoscale_tabbarview.dart'; import 'tabs/property_tab.dart'; import 'tabs/finance_tab.dart'; @@ -62,7 +61,8 @@ void _openMapModal(BuildContext context, dynamic lat, dynamic lng) { ), children: [ TileLayer( - urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], ), MarkerLayer( @@ -86,7 +86,8 @@ void _openMapModal(BuildContext context, dynamic lat, dynamic lng) { right: 20, child: FloatingActionButton( onPressed: () { - final googleStreetViewUrl = 'https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=$latitude,$longitude'; + final googleStreetViewUrl = + 'https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=$latitude,$longitude'; UrlUtils.launchURL(googleStreetViewUrl); }, backgroundColor: Theme.of(context).primaryColor, @@ -110,17 +111,18 @@ class TokenDetailsWidget extends StatefulWidget { final ScrollController scrollController; const TokenDetailsWidget({ - Key? key, + super.key, required this.token, required this.convertToSquareMeters, required this.scrollController, - }) : super(key: key); + }); @override _TokenDetailsWidgetState createState() => _TokenDetailsWidgetState(); } -class _TokenDetailsWidgetState extends State with SingleTickerProviderStateMixin { +class _TokenDetailsWidgetState extends State + with SingleTickerProviderStateMixin { int _currentCarouselIndex = 0; late TabController _tabController; @@ -141,9 +143,11 @@ class _TokenDetailsWidgetState extends State with SingleTick final currencyUtils = Provider.of(context, listen: false); switch (index) { case 0: - return buildPropertiesTab(context, widget.token, widget.convertToSquareMeters); + return buildPropertiesTab( + context, widget.token, widget.convertToSquareMeters); case 1: - return buildFinanceTab(context, widget.token, widget.convertToSquareMeters); + return buildFinanceTab( + context, widget.token, widget.convertToSquareMeters); case 2: return MarketTab(token: widget.token); case 3: @@ -151,7 +155,8 @@ class _TokenDetailsWidgetState extends State with SingleTick case 4: return buildInsightsTab(context, widget.token); case 5: - return buildHistoryTab(context, widget.token, dataManager.isLoadingTransactions); + return buildHistoryTab( + context, widget.token, dataManager.isLoadingTransactions); default: return const SizedBox.shrink(); } @@ -180,7 +185,7 @@ class _TokenDetailsWidgetState extends State with SingleTick height: 5, margin: const EdgeInsets.only(top: 10, bottom: 5), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.5), + color: Colors.grey.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(2.5), ), ), @@ -188,12 +193,17 @@ class _TokenDetailsWidgetState extends State with SingleTick // Image et infos principales Padding( padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 0), - child: widget.token['imageLink'] != null && widget.token['imageLink'].isNotEmpty + child: widget.token['imageLink'] != null && + widget.token['imageLink'].isNotEmpty ? Column( children: [ GestureDetector( onTap: () { - final List imageLinks = widget.token['imageLink'] is String ? [widget.token['imageLink']] : List.from(widget.token['imageLink']); + final List imageLinks = + widget.token['imageLink'] is String + ? [widget.token['imageLink']] + : List.from( + widget.token['imageLink']); Navigator.of(context).push( MaterialPageRoute( builder: (_) => FullScreenCarousel( @@ -202,16 +212,25 @@ class _TokenDetailsWidgetState extends State with SingleTick ), ); }, - child: StatefulBuilder(builder: (context, setState) { - final List imageLinks = widget.token['imageLink'] is String ? [widget.token['imageLink']] : List.from(widget.token['imageLink']); + child: + StatefulBuilder(builder: (context, setState) { + final List imageLinks = + widget.token['imageLink'] is String + ? [widget.token['imageLink']] + : List.from( + widget.token['imageLink']); return Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(15.0), child: CarouselSlider( options: CarouselOptions( - height: MediaQuery.of(context).size.height * 0.22, - enableInfiniteScroll: imageLinks.length > 1, + height: MediaQuery.of(context) + .size + .height * + 0.22, + enableInfiniteScroll: + imageLinks.length > 1, enlargeCenterPage: true, onPageChanged: (index, reason) { setState(() { @@ -220,13 +239,16 @@ class _TokenDetailsWidgetState extends State with SingleTick }, viewportFraction: 0.9, ), - items: imageLinks.map((imageUrl) { + items: + imageLinks.map((imageUrl) { return Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), + borderRadius: + BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black + .withValues(alpha: 0.1), spreadRadius: 1, blurRadius: 5, offset: const Offset(0, 2), @@ -236,25 +258,39 @@ class _TokenDetailsWidgetState extends State with SingleTick child: Stack( children: [ ClipRRect( - borderRadius: BorderRadius.circular(15), + borderRadius: + BorderRadius.circular(15), child: kIsWeb - ? ShowNetworkImage(imageSrc: imageUrl, mobileBoxFit: BoxFit.cover, mobileWidth: double.infinity) + ? ShowNetworkImage( + imageSrc: imageUrl, + mobileBoxFit: + BoxFit.cover, + mobileWidth: + double.infinity) : CachedNetworkImage( imageUrl: imageUrl, - width: double.infinity, + width: + double.infinity, fit: BoxFit.cover, - errorWidget: (context, url, error) => const Icon(Icons.error), + errorWidget: (context, + url, error) => + const Icon( + Icons.error), ), ), if (kIsWeb) Positioned.fill( child: GestureDetector( - behavior: HitTestBehavior.translucent, + behavior: HitTestBehavior + .translucent, onTap: () { - Navigator.of(context).push( + Navigator.of(context) + .push( MaterialPageRoute( - builder: (_) => FullScreenCarousel( - imageLinks: imageLinks, + builder: (_) => + FullScreenCarousel( + imageLinks: + imageLinks, ), ), ); @@ -271,15 +307,26 @@ class _TokenDetailsWidgetState extends State with SingleTick Container( margin: const EdgeInsets.only(top: 10), child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: imageLinks.asMap().entries.map((entry) { + mainAxisAlignment: + MainAxisAlignment.center, + children: imageLinks + .asMap() + .entries + .map((entry) { return Container( width: 8.0, height: 8.0, - margin: const EdgeInsets.symmetric(horizontal: 4.0), + margin: + const EdgeInsets.symmetric( + horizontal: 4.0), decoration: BoxDecoration( shape: BoxShape.circle, - color: _currentCarouselIndex == entry.key ? Theme.of(context).primaryColor : Colors.grey.withOpacity(0.4), + color: _currentCarouselIndex == + entry.key + ? Theme.of(context) + .primaryColor + : Colors.grey + .withValues(alpha: 0.4), ), ); }).toList(), @@ -294,11 +341,12 @@ class _TokenDetailsWidgetState extends State with SingleTick : Container( height: 200, decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(15), ), child: const Center( - child: Icon(Icons.image_not_supported, size: 50, color: Colors.grey), + child: Icon(Icons.image_not_supported, + size: 50, color: Colors.grey), ), ), ), @@ -328,16 +376,21 @@ class _TokenDetailsWidgetState extends State with SingleTick crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.token['shortName'] ?? S.of(context).nameUnavailable, + widget.token['shortName'] ?? + S.of(context).nameUnavailable, style: TextStyle( fontSize: 16 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.titleLarge?.color, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.token['city'] != null || widget.token['regionCode'] != null) + if (widget.token['city'] != null || + widget.token['regionCode'] != null) Row( children: [ Icon( @@ -346,17 +399,25 @@ class _TokenDetailsWidgetState extends State with SingleTick color: Colors.grey[600], ), // Drapeau de l'état américain si disponible - if (widget.token['regionCode'] != null && widget.token['country']?.toLowerCase() == 'usa') + if (widget.token['regionCode'] != null && + widget.token['country'] + ?.toLowerCase() == + 'usa') Container( - margin: const EdgeInsets.only(left: 4.0, right: 2.0), + margin: const EdgeInsets.only( + left: 4.0, right: 2.0), child: Image.asset( 'assets/states/${widget.token['regionCode'].toLowerCase()}.png', - width: 16 + appState.getTextSizeOffset(), - height: 16 + appState.getTextSizeOffset(), - errorBuilder: (context, error, stackTrace) { + width: + 16 + appState.getTextSizeOffset(), + height: + 16 + appState.getTextSizeOffset(), + errorBuilder: + (context, error, stackTrace) { return Icon( Icons.location_city, - size: 14 + appState.getTextSizeOffset(), + size: 14 + + appState.getTextSizeOffset(), color: Colors.grey[600], ); }, @@ -366,10 +427,14 @@ class _TokenDetailsWidgetState extends State with SingleTick Text( [ widget.token['city'], - widget.token['regionCode'] != null ? Parameters.getRegionDisplayName(widget.token['regionCode']) : null + widget.token['regionCode'] != null + ? Parameters.getRegionDisplayName( + widget.token['regionCode']) + : null ].where((e) => e != null).join(', '), style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), + fontSize: + 12 + appState.getTextSizeOffset(), color: Colors.grey[600], ), maxLines: 1, @@ -382,15 +447,19 @@ class _TokenDetailsWidgetState extends State with SingleTick ], ), // Conteneur pour les informations financières - if (widget.token['amount'] != null || widget.token['totalValue'] != null) + if (widget.token['amount'] != null || + widget.token['totalValue'] != null) Container( margin: const EdgeInsets.only(top: 8.0), - padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), + padding: const EdgeInsets.symmetric( + vertical: 6.0, horizontal: 10.0), decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.9), + color: Theme.of(context) + .cardColor + .withValues(alpha: 0.9), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), width: 0.5, ), ), @@ -408,19 +477,24 @@ class _TokenDetailsWidgetState extends State with SingleTick ), const SizedBox(width: 4), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).amount, style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), + fontSize: 11 + + appState.getTextSizeOffset(), color: Colors.grey, ), ), Text( - widget.token['amount']?.toStringAsFixed(2) ?? "0", + widget.token['amount'] + ?.toStringAsFixed(2) ?? + "0", style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: 14 + + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -430,12 +504,14 @@ class _TokenDetailsWidgetState extends State with SingleTick ), ), // Séparateur vertical - if (widget.token['amount'] != null && widget.token['totalValue'] != null) + if (widget.token['amount'] != null && + widget.token['totalValue'] != null) Container( height: 24, width: 1, - color: Colors.grey.withOpacity(0.3), - margin: const EdgeInsets.symmetric(horizontal: 8), + color: Colors.grey.withValues(alpha: 0.3), + margin: + const EdgeInsets.symmetric(horizontal: 8), ), // Valeur totale if (widget.token['totalValue'] != null) @@ -445,25 +521,36 @@ class _TokenDetailsWidgetState extends State with SingleTick Icon( Icons.attach_money, size: 16, - color: Theme.of(context).primaryColor.withOpacity(0.8), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.8), ), const SizedBox(width: 4), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( "Value", style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor.withOpacity(0.8), + fontSize: 11 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.8), ), ), Text( - currencyUtils.formatCurrency(currencyUtils.convert(widget.token['totalValue']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert( + widget.token['totalValue']), + currencyUtils.currencySymbol), style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: 14 + + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), ], @@ -499,7 +586,8 @@ class _TokenDetailsWidgetState extends State with SingleTick borderRadius: BorderRadius.circular(15), ), splashFactory: NoSplash.splashFactory, - overlayColor: MaterialStateProperty.resolveWith((states) => null), + overlayColor: + WidgetStateProperty.resolveWith((states) => null), labelStyle: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, @@ -509,12 +597,16 @@ class _TokenDetailsWidgetState extends State with SingleTick fontWeight: FontWeight.normal, ), tabs: [ - _buildTabSegment(icon: Icons.home_outlined, label: "Property"), - _buildTabSegment(icon: Icons.attach_money_outlined, label: "Finance"), + _buildTabSegment( + icon: Icons.home_outlined, label: "Property"), + _buildTabSegment( + icon: Icons.attach_money_outlined, label: "Finance"), _buildTabSegment(icon: Icons.store_outlined, label: "Market"), _buildTabSegment(icon: Icons.info_outline, label: "Info"), - _buildTabSegment(icon: Icons.insights_outlined, label: "Insights"), - _buildTabSegment(icon: Icons.history_outlined, label: "History"), + _buildTabSegment( + icon: Icons.insights_outlined, label: "Insights"), + _buildTabSegment( + icon: Icons.history_outlined, label: "History"), ], onTap: (index) { setState(() {}); // Pour rafraîchir le contenu de l'onglet @@ -554,16 +646,19 @@ class _TabBarDelegate extends SliverPersistentHeaderDelegate { double get maxExtent => 35; @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( height: 35, margin: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.8) : Theme.of(context).cardColor.withOpacity(0.95), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.8) + : Theme.of(context).cardColor.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), @@ -574,10 +669,12 @@ class _TabBarDelegate extends SliverPersistentHeaderDelegate { } @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true; + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + true; } -Future showTokenDetails(BuildContext context, Map token) async { +Future showTokenDetails( + BuildContext context, Map token) async { final dataManager = Provider.of(context, listen: false); final currencyUtils = Provider.of(context, listen: false); final prefs = await SharedPreferences.getInstance(); @@ -604,13 +701,11 @@ Future showTokenDetails(BuildContext context, Map token) ), child: Stack( children: [ - - // hauteur de la barre d'action - TokenDetailsWidget( - token: token, - convertToSquareMeters: convertToSquareMeters, - scrollController: scrollController, - + // hauteur de la barre d'action + TokenDetailsWidget( + token: token, + convertToSquareMeters: convertToSquareMeters, + scrollController: scrollController, ), Positioned( left: 0, @@ -621,7 +716,7 @@ Future showTokenDetails(BuildContext context, Map token) color: Theme.of(context).cardColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, -2), ), @@ -634,13 +729,18 @@ Future showTokenDetails(BuildContext context, Map token) SizedBox( height: 32, child: ElevatedButton( - onPressed: () => UrlUtils.launchURL(token['marketplaceLink']), + onPressed: () => + UrlUtils.launchURL(token['marketplaceLink']), style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Theme.of(context).primaryColor, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 8), textStyle: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), child: Text(S.of(context).viewOnRealT), @@ -650,12 +750,18 @@ Future showTokenDetails(BuildContext context, Map token) SizedBox( height: 32, child: ElevatedButton( - onPressed: () => _openMapModal(context, token['lat'], token['lng']), + onPressed: () => _openMapModal( + context, token['lat'], token['lng']), style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.green, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - textStyle: TextStyle(fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset()), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 8), + textStyle: TextStyle( + fontSize: 13 + + Provider.of(context, + listen: false) + .getTextSizeOffset()), ), child: Text(S.of(context).viewOnMap), ), diff --git a/lib/modals/token_details/tabs/finance_tab.dart b/lib/modals/token_details/tabs/finance_tab.dart index 595207d..70cb4c4 100644 --- a/lib/modals/token_details/tabs/finance_tab.dart +++ b/lib/modals/token_details/tabs/finance_tab.dart @@ -5,7 +5,8 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; -Widget buildFinanceTab(BuildContext context, Map token, bool convertToSquareMeters) { +Widget buildFinanceTab(BuildContext context, Map token, + bool convertToSquareMeters) { final appState = Provider.of(context, listen: false); final dataManager = Provider.of(context, listen: false); final currencyUtils = Provider.of(context, listen: false); @@ -16,15 +17,17 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co (token['renovationReserve']?.toDouble() ?? 0.0) + (token['miscellaneousCosts']?.toDouble() ?? 0.0); - double totalRentCosts = (token['propertyMaintenanceMonthly']?.toDouble() ?? 0.0) + - (token['propertyManagement']?.toDouble() ?? 0.0) + - (token['realtPlatform']?.toDouble() ?? 0.0) + - (token['insurance']?.toDouble() ?? 0.0) + - (token['propertyTaxes']?.toDouble() ?? 0.0); + double totalRentCosts = + (token['propertyMaintenanceMonthly']?.toDouble() ?? 0.0) + + (token['propertyManagement']?.toDouble() ?? 0.0) + + (token['realtPlatform']?.toDouble() ?? 0.0) + + (token['insurance']?.toDouble() ?? 0.0) + + (token['propertyTaxes']?.toDouble() ?? 0.0); // Contrôle de la visibilité des détails final ValueNotifier showDetailsNotifier = ValueNotifier(false); - final ValueNotifier showRentDetailsNotifier = ValueNotifier(false); + final ValueNotifier showRentDetailsNotifier = + ValueNotifier(false); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), @@ -39,7 +42,9 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).totalInvestment, - currencyUtils.formatCurrency(currencyUtils.convert(token['totalInvestment']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(token['totalInvestment']), + currencyUtils.currencySymbol), icon: Icons.monetization_on, iconColor: Colors.green, ), @@ -47,9 +52,11 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co // Section des dépenses totales GestureDetector( - onTap: () => showDetailsNotifier.value = !showDetailsNotifier.value, + onTap: () => + showDetailsNotifier.value = !showDetailsNotifier.value, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -59,7 +66,7 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co width: 32, height: 32, decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -74,7 +81,8 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.w300, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: + Theme.of(context).textTheme.bodyMedium?.color, ), ), const SizedBox(width: 4), @@ -82,7 +90,9 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co valueListenable: showDetailsNotifier, builder: (context, showDetails, child) { return Icon( - showDetails ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, + showDetails + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, color: Colors.grey, size: 18 + appState.getTextSizeOffset(), ); @@ -91,7 +101,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co ], ), Text( - currencyUtils.formatCurrency(currencyUtils.convert(token['totalInvestment'] - token['underlyingAssetPrice']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(token['totalInvestment'] - + token['underlyingAssetPrice']), + currencyUtils.currencySymbol), style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.w400, @@ -116,7 +129,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).realtListingFee, - currencyUtils.formatCurrency(currencyUtils.convert(token['realtListingFee'] ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils + .convert(token['realtListingFee'] ?? 0), + currencyUtils.currencySymbol), icon: Icons.circle, iconColor: Colors.red.shade300, isExpenseItem: true, @@ -124,7 +140,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).initialMaintenanceReserve, - currencyUtils.formatCurrency(currencyUtils.convert(token['initialMaintenanceReserve'] ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert( + token['initialMaintenanceReserve'] ?? 0), + currencyUtils.currencySymbol), icon: Icons.circle, iconColor: Colors.orange.shade300, isExpenseItem: true, @@ -132,7 +151,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).renovationReserve, - currencyUtils.formatCurrency(currencyUtils.convert(token['renovationReserve'] ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils + .convert(token['renovationReserve'] ?? 0), + currencyUtils.currencySymbol), icon: Icons.circle, iconColor: Colors.purple.shade300, isExpenseItem: true, @@ -140,7 +162,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).miscellaneousCosts, - currencyUtils.formatCurrency(currencyUtils.convert(token['miscellaneousCosts'] ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert( + token['miscellaneousCosts'] ?? 0), + currencyUtils.currencySymbol), icon: Icons.circle, iconColor: Colors.amber.shade300, isExpenseItem: true, @@ -148,7 +173,13 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).others, - currencyUtils.formatCurrency(currencyUtils.convert((token['totalInvestment'] - token['underlyingAssetPrice'] - totalCosts) ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert( + (token['totalInvestment'] - + token['underlyingAssetPrice'] - + totalCosts) ?? + 0), + currencyUtils.currencySymbol), icon: Icons.circle, iconColor: Colors.grey.shade400, isExpenseItem: true, @@ -167,7 +198,7 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: const Offset(0, 1), ), @@ -182,10 +213,13 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co (token['initialMaintenanceReserve'] ?? 0).toDouble(), (token['renovationReserve'] ?? 0).toDouble(), (token['miscellaneousCosts'] ?? 0).toDouble(), - ((token['totalInvestment'] ?? 0).toDouble() - (token['underlyingAssetPrice'] ?? 0).toDouble() - totalCosts), + ((token['totalInvestment'] ?? 0).toDouble() - + (token['underlyingAssetPrice'] ?? 0).toDouble() - + totalCosts), ]; final double sum = totalCosts > 0 ? totalCosts : 1; - final List widths = parts.map((v) => v / sum * totalWidth).toList(); + final List widths = + parts.map((v) => v / sum * totalWidth).toList(); return Row( children: [ Container( @@ -208,7 +242,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.orange.shade600, Colors.orange.shade400], + colors: [ + Colors.orange.shade600, + Colors.orange.shade400 + ], ), ), ), @@ -220,7 +257,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.purple.shade600, Colors.purple.shade400], + colors: [ + Colors.purple.shade600, + Colors.purple.shade400 + ], ), ), ), @@ -232,7 +272,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.amber.shade600, Colors.amber.shade400], + colors: [ + Colors.amber.shade600, + Colors.amber.shade400 + ], ), ), ), @@ -244,7 +287,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.grey.shade500, Colors.grey.shade400], + colors: [ + Colors.grey.shade500, + Colors.grey.shade400 + ], ), ), ), @@ -255,8 +301,14 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co ), const Divider(height: 1, thickness: 0.5), - _buildDetailRow(context, S.of(context).underlyingAssetPrice, currencyUtils.formatCurrency(currencyUtils.convert(token['underlyingAssetPrice'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.home, iconColor: Colors.blue), + _buildDetailRow( + context, + S.of(context).underlyingAssetPrice, + currencyUtils.formatCurrency( + currencyUtils.convert(token['underlyingAssetPrice'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.home, + iconColor: Colors.blue), ], ), @@ -267,16 +319,24 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co context, title: S.of(context).rents, children: [ - _buildDetailRow(context, S.of(context).grossRentMonth, currencyUtils.formatCurrency(currencyUtils.convert(token['grossRentMonth'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.attach_money, iconColor: Colors.green), + _buildDetailRow( + context, + S.of(context).grossRentMonth, + currencyUtils.formatCurrency( + currencyUtils.convert(token['grossRentMonth'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.attach_money, + iconColor: Colors.green), const Divider(height: 1, thickness: 0.5), // Détails des dépenses de loyer GestureDetector( - onTap: () => showRentDetailsNotifier.value = !showRentDetailsNotifier.value, + onTap: () => showRentDetailsNotifier.value = + !showRentDetailsNotifier.value, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -286,7 +346,7 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co width: 32, height: 32, decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -301,7 +361,8 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.w300, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: + Theme.of(context).textTheme.bodyMedium?.color, ), ), const SizedBox(width: 4), @@ -309,7 +370,9 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co valueListenable: showRentDetailsNotifier, builder: (context, showDetails, child) { return Icon( - showDetails ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, + showDetails + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, color: Colors.grey, size: 18 + appState.getTextSizeOffset(), ); @@ -340,21 +403,69 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co child: showDetails ? Column( children: [ - _buildDetailRow(context, S.of(context).propertyMaintenanceMonthly, - currencyUtils.formatCurrency(currencyUtils.convert(token['propertyMaintenanceMonthly'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.deepOrange.shade300, isExpenseItem: true), _buildDetailRow( - context, S.of(context).propertyManagement, currencyUtils.formatCurrency(currencyUtils.convert(token['propertyManagement'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.amber.shade300, isExpenseItem: true), - _buildDetailRow(context, S.of(context).realtPlatform, currencyUtils.formatCurrency(currencyUtils.convert(token['realtPlatform'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.orange.shade300, isExpenseItem: true), - _buildDetailRow(context, S.of(context).insurance, currencyUtils.formatCurrency(currencyUtils.convert(token['insurance'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.purple.shade300, isExpenseItem: true), - _buildDetailRow(context, S.of(context).propertyTaxes, currencyUtils.formatCurrency(currencyUtils.convert(token['propertyTaxes'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.red.shade300, isExpenseItem: true), - _buildDetailRow(context, S.of(context).others, - currencyUtils.formatCurrency((currencyUtils.convert(token['grossRentMonth'] - token['netRentMonth'] - totalRentCosts)), currencyUtils.currencySymbol), - icon: Icons.circle, iconColor: Colors.grey.shade400, isExpenseItem: true), + context, + S.of(context).propertyMaintenanceMonthly, + currencyUtils.formatCurrency( + currencyUtils.convert( + token['propertyMaintenanceMonthly'] ?? + 0), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.deepOrange.shade300, + isExpenseItem: true), + _buildDetailRow( + context, + S.of(context).propertyManagement, + currencyUtils.formatCurrency( + currencyUtils.convert( + token['propertyManagement'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.amber.shade300, + isExpenseItem: true), + _buildDetailRow( + context, + S.of(context).realtPlatform, + currencyUtils.formatCurrency( + currencyUtils + .convert(token['realtPlatform'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.orange.shade300, + isExpenseItem: true), + _buildDetailRow( + context, + S.of(context).insurance, + currencyUtils.formatCurrency( + currencyUtils + .convert(token['insurance'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.purple.shade300, + isExpenseItem: true), + _buildDetailRow( + context, + S.of(context).propertyTaxes, + currencyUtils.formatCurrency( + currencyUtils + .convert(token['propertyTaxes'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.red.shade300, + isExpenseItem: true), + _buildDetailRow( + context, + S.of(context).others, + currencyUtils.formatCurrency( + (currencyUtils.convert( + token['grossRentMonth'] - + token['netRentMonth'] - + totalRentCosts)), + currencyUtils.currencySymbol), + icon: Icons.circle, + iconColor: Colors.grey.shade400, + isExpenseItem: true), ], ) : const SizedBox.shrink(), @@ -369,7 +480,7 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: const Offset(0, 1), ), @@ -385,10 +496,13 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co (token['realtPlatform'] ?? 0).toDouble(), (token['insurance'] ?? 0).toDouble(), (token['propertyTaxes'] ?? 0).toDouble(), - ((token['grossRentMonth'] ?? 0.0) - (token['netRentMonth'] ?? 0.0) - totalRentCosts), + ((token['grossRentMonth'] ?? 0.0) - + (token['netRentMonth'] ?? 0.0) - + totalRentCosts), ]; final double sum = totalRentCosts != 0 ? totalRentCosts : 1; - final List widths = parts.map((v) => v / sum * totalWidth).toList(); + final List widths = + parts.map((v) => v / sum * totalWidth).toList(); return Row( children: [ Container( @@ -399,7 +513,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.deepOrange.shade600, Colors.deepOrange.shade400], + colors: [ + Colors.deepOrange.shade600, + Colors.deepOrange.shade400 + ], ), ), ), @@ -411,7 +528,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.amber.shade600, Colors.amber.shade400], + colors: [ + Colors.amber.shade600, + Colors.amber.shade400 + ], ), ), ), @@ -423,7 +543,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.orange.shade600, Colors.orange.shade400], + colors: [ + Colors.orange.shade600, + Colors.orange.shade400 + ], ), ), ), @@ -435,7 +558,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.purple.shade600, Colors.purple.shade400], + colors: [ + Colors.purple.shade600, + Colors.purple.shade400 + ], ), ), ), @@ -459,7 +585,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.grey.shade500, Colors.grey.shade400], + colors: [ + Colors.grey.shade500, + Colors.grey.shade400 + ], ), ), ), @@ -470,8 +599,14 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co ), const Divider(height: 1, thickness: 0.5), - _buildDetailRow(context, S.of(context).netRentMonth, currencyUtils.formatCurrency(currencyUtils.convert(token['netRentMonth'] ?? 0), currencyUtils.currencySymbol), - icon: Icons.account_balance, iconColor: Colors.green), + _buildDetailRow( + context, + S.of(context).netRentMonth, + currencyUtils.formatCurrency( + currencyUtils.convert(token['netRentMonth'] ?? 0), + currencyUtils.currencySymbol), + icon: Icons.account_balance, + iconColor: Colors.green), ], ), @@ -485,12 +620,16 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).initialPrice, - currencyUtils.formatCurrency(currencyUtils.convert(token['initPrice']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(token['initPrice']), + currencyUtils.currencySymbol), icon: Icons.price_change_sharp, iconColor: Colors.indigo, trailing: IconButton( padding: EdgeInsets.zero, - icon: Icon(Icons.edit, color: Colors.grey, size: 16 + appState.getTextSizeOffset()), + icon: Icon(Icons.edit, + color: Colors.grey, + size: 16 + appState.getTextSizeOffset()), onPressed: () { _showEditPriceBottomModal(context, token, dataManager); }, @@ -500,7 +639,9 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).realtActualPrice, - currencyUtils.formatCurrency(currencyUtils.convert(token['tokenPrice']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(token['tokenPrice']), + currencyUtils.currencySymbol), icon: Icons.price_change_sharp, iconColor: Colors.teal, ), @@ -512,7 +653,10 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co '${currencyUtils.formatCurrency(currencyUtils.convert((token['yamAverageValue'])), currencyUtils.currencySymbol)} (${((token['yamAverageValue'] / token['initPrice'] - 1) * 100).toStringAsFixed(0)}%)', icon: Icons.price_change_sharp, iconColor: Colors.blueGrey, - textColor: (token['yamAverageValue'] * token['amount']) > token['totalValue'] ? Colors.green : Colors.red, + textColor: (token['yamAverageValue'] * token['amount']) > + token['totalValue'] + ? Colors.green + : Colors.red, ), const Divider(height: 1, thickness: 0.5), _buildDetailRow( @@ -526,7 +670,9 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co _buildDetailRow( context, S.of(context).totalRentReceived, - currencyUtils.formatCurrency(currencyUtils.convert(token['totalRentReceived'] ?? 0), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(token['totalRentReceived'] ?? 0), + currencyUtils.currencySymbol), icon: Icons.receipt_long, iconColor: Colors.green, ), @@ -546,7 +692,8 @@ Widget buildFinanceTab(BuildContext context, Map token, bool co } // Méthode pour construire une section avec carte, comme dans property_tab.dart -Widget _buildSectionCard(BuildContext context, {required String title, required List children}) { +Widget _buildSectionCard(BuildContext context, + {required String title, required List children}) { return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( @@ -554,7 +701,7 @@ Widget _buildSectionCard(BuildContext context, {required String title, required borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -581,7 +728,12 @@ Widget _buildSectionCard(BuildContext context, {required String title, required } // Méthode pour construire les lignes de détails, comme dans property_tab.dart -Widget _buildDetailRow(BuildContext context, String label, String value, {IconData? icon, Color? iconColor, Color? textColor, Widget? trailing, bool isExpenseItem = false}) { +Widget _buildDetailRow(BuildContext context, String label, String value, + {IconData? icon, + Color? iconColor, + Color? textColor, + Widget? trailing, + bool isExpenseItem = false}) { final appState = Provider.of(context, listen: false); return Padding( @@ -602,7 +754,8 @@ Widget _buildDetailRow(BuildContext context, String label, String value, {IconDa width: 32, height: 32, decoration: BoxDecoration( - color: (iconColor ?? Colors.blue).withOpacity(0.1), + color: + (iconColor ?? Colors.blue).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -628,7 +781,9 @@ Widget _buildDetailRow(BuildContext context, String label, String value, {IconDa style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.w400, - color: isExpenseItem ? Colors.red : (textColor ?? Theme.of(context).textTheme.bodyLarge?.color), + color: isExpenseItem + ? Colors.red + : (textColor ?? Theme.of(context).textTheme.bodyLarge?.color), ), ), ], @@ -637,7 +792,8 @@ Widget _buildDetailRow(BuildContext context, String label, String value, {IconDa } // Méthode pour afficher le BottomModal de modification du prix -void _showEditPriceBottomModal(BuildContext context, Map token, DataManager dataManager) { +void _showEditPriceBottomModal( + BuildContext context, Map token, DataManager dataManager) { final TextEditingController priceController = TextEditingController( text: token['initPrice']?.toString() ?? '0.00', ); @@ -677,7 +833,8 @@ void _showEditPriceBottomModal(BuildContext context, Map token, Text( S.of(context).initialPrice, style: TextStyle( - fontSize: 20 + Provider.of(context).getTextSizeOffset(), + fontSize: + 20 + Provider.of(context).getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -686,7 +843,8 @@ void _showEditPriceBottomModal(BuildContext context, Map token, Text( S.of(context).initialPriceModified_description, style: TextStyle( - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: + 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey[600], ), textAlign: TextAlign.center, @@ -714,7 +872,8 @@ void _showEditPriceBottomModal(BuildContext context, Map token, ), suffixText: '\$', floatingLabelBehavior: FloatingLabelBehavior.always, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), ), const SizedBox(height: 24), @@ -723,73 +882,72 @@ void _showEditPriceBottomModal(BuildContext context, Map token, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // Bouton pour sauvegarder - - ElevatedButton.icon( - onPressed: () { - final newPrice = double.tryParse(priceController.text); - if (newPrice != null) { - dataManager.setCustomInitPrice(token['uuid'], newPrice); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).initialPriceUpdated), - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).enterValidNumber), - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - }, - icon: const Icon(Icons.check, color: Colors.white), - label: Text(S.of(context).save), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(vertical: 6), - ), - ), - - const SizedBox(width: 12), - // Bouton pour supprimer - - ElevatedButton.icon( - onPressed: () { - dataManager.removeCustomInitPrice(token['uuid']); + + ElevatedButton.icon( + onPressed: () { + final newPrice = double.tryParse(priceController.text); + if (newPrice != null) { + dataManager.setCustomInitPrice(token['uuid'], newPrice); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(S.of(context).initialPriceRemoved), + content: Text(S.of(context).initialPriceUpdated), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), ); - }, - icon: const Icon(Icons.delete, color: Colors.white), - label: Text('Supprimer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).enterValidNumber), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + }, + icon: const Icon(Icons.check, color: Colors.white), + label: Text(S.of(context).save), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 6), + ), + ), + + const SizedBox(width: 12), + // Bouton pour supprimer + + ElevatedButton.icon( + onPressed: () { + dataManager.removeCustomInitPrice(token['uuid']); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).initialPriceRemoved), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), - padding: const EdgeInsets.symmetric(vertical: 6), + ); + }, + icon: const Icon(Icons.delete, color: Colors.white), + label: Text('Supprimer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), + padding: const EdgeInsets.symmetric(vertical: 6), ), - + ), ], ), ], diff --git a/lib/modals/token_details/tabs/history_tab.dart b/lib/modals/token_details/tabs/history_tab.dart index ac7cf57..f64eecb 100644 --- a/lib/modals/token_details/tabs/history_tab.dart +++ b/lib/modals/token_details/tabs/history_tab.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; @@ -18,8 +17,12 @@ Widget buildHistoryTab(BuildContext context, Map token, if (token['transactions'] != null && token['transactions'].isNotEmpty) { List sortedTransactions = List.from(token['transactions']); sortedTransactions.sort((a, b) { - final dateA = a['dateTime'] != null ? DateTime.parse(a['dateTime'].toString()) : DateTime.now(); - final dateB = b['dateTime'] != null ? DateTime.parse(b['dateTime'].toString()) : DateTime.now(); + final dateA = a['dateTime'] != null + ? DateTime.parse(a['dateTime'].toString()) + : DateTime.now(); + final dateB = b['dateTime'] != null + ? DateTime.parse(b['dateTime'].toString()) + : DateTime.now(); return dateB.compareTo(dateA); }); token['transactions'] = sortedTransactions; @@ -29,22 +32,23 @@ Widget buildHistoryTab(BuildContext context, Map token, final String tokenUuid = token['uuid'] ?? token['gnosisContract'] ?? ''; List> rawTokenHistory = []; List> tokenChanges = []; - + if (tokenUuid.isNotEmpty && dataManager.tokenHistoryData.isNotEmpty) { print("🔍 Recherche historique pour token: $tokenUuid"); - + // Utiliser la méthode existante du DataManager rawTokenHistory = dataManager.getTokenHistory(tokenUuid); print("📋 Historique brut trouvé: ${rawTokenHistory.length} entrées"); - + // Détecter les changements entre les entrées consécutives for (int i = 1; i < rawTokenHistory.length; i++) { var previous = rawTokenHistory[i]; // Plus ancien var current = rawTokenHistory[i - 1]; // Plus récent - + // Détecter les changements dans les champs importants - List> changes = _detectTokenChanges(previous, current, token); - + List> changes = + _detectTokenChanges(previous, current, token); + if (changes.isNotEmpty) { tokenChanges.add({ 'date': current['date'], @@ -52,8 +56,9 @@ Widget buildHistoryTab(BuildContext context, Map token, }); } } - - print("📊 Changements détectés: ${tokenChanges.length} dates avec modifications"); + + print( + "📊 Changements détectés: ${tokenChanges.length} dates avec modifications"); } return Padding( @@ -68,73 +73,81 @@ Widget buildHistoryTab(BuildContext context, Map token, children: [ // Gestion de l'état de chargement avec Shimmer if (isLoadingTransactions) - ...List.generate(5, (index) => // Placeholder pour 5 items simulés - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - child: Card( - elevation: 0, // Style iOS plat - margin: const EdgeInsets.only(bottom: 8.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), // Coins arrondis style iOS - ), - color: Theme.of(context).cardColor, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - ShimmerUtils.standardShimmer( - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.grey[300], - shape: BoxShape.circle, - ), - ), + ...List.generate( + 5, + (index) => // Placeholder pour 5 items simulés + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 4.0), + child: Card( + elevation: 0, // Style iOS plat + margin: const EdgeInsets.only(bottom: 8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 12), // Coins arrondis style iOS ), - const SizedBox(width: 16.0), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( children: [ ShimmerUtils.standardShimmer( child: Container( - width: double.infinity, - height: 14, + width: 40, + height: 40, decoration: BoxDecoration( color: Colors.grey[300], - borderRadius: BorderRadius.circular(7), + shape: BoxShape.circle, ), ), ), - const SizedBox(height: 8.0), - ShimmerUtils.standardShimmer( - child: Container( - width: 100, - height: 12, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(6), - ), + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ShimmerUtils.standardShimmer( + child: Container( + width: double.infinity, + height: 14, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: + BorderRadius.circular(7), + ), + ), + ), + const SizedBox(height: 8.0), + ShimmerUtils.standardShimmer( + child: Container( + width: 100, + height: 12, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: + BorderRadius.circular(6), + ), + ), + ), + ], ), ), ], ), ), - ], - ), - ), - ), - ) - ) - else if (token['transactions'] != null && token['transactions'].isNotEmpty) + ), + )) + else if (token['transactions'] != null && + token['transactions'].isNotEmpty) ...token['transactions'].map((transaction) { final price = '${currencyUtils.convert(transaction['price'] ?? token['tokenPrice']).toStringAsFixed(2)} ${currencyUtils.currencySymbol}'; final amount = transaction['amount'] ?? 0.0; - final transactionType = transaction.containsKey('transactionType') - ? transaction['transactionType'] - : S.of(context).unknownTransaction; + final transactionType = + transaction.containsKey('transactionType') + ? transaction['transactionType'] + : S.of(context).unknownTransaction; final dateTime = transaction['dateTime'] != null ? DateFormat('yyyy-MM-dd HH:mm') @@ -149,7 +162,8 @@ Widget buildHistoryTab(BuildContext context, Map token, icon = Icons.shopping_cart; iconColor = Colors.white; bgColor = Colors.blue; - } else if (transactionType == DataManager.transactionTypeTransfer) { + } else if (transactionType == + DataManager.transactionTypeTransfer) { icon = Icons.swap_horiz; iconColor = Colors.white; bgColor = Colors.grey; @@ -165,7 +179,8 @@ Widget buildHistoryTab(BuildContext context, Map token, // Afficher chaque transaction dans une belle carte style iOS return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 4.0), child: Container( margin: const EdgeInsets.only(bottom: 8.0), decoration: BoxDecoration( @@ -173,7 +188,7 @@ Widget buildHistoryTab(BuildContext context, Map token, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2), ), @@ -187,8 +202,8 @@ Widget buildHistoryTab(BuildContext context, Map token, onTap: () { // Optionnellement: afficher plus de détails sur la transaction }, - splashColor: bgColor.withOpacity(0.1), - highlightColor: bgColor.withOpacity(0.05), + splashColor: bgColor.withValues(alpha: 0.1), + highlightColor: bgColor.withValues(alpha: 0.05), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( @@ -203,7 +218,7 @@ Widget buildHistoryTab(BuildContext context, Map token, ), child: Center( child: Icon( - icon, + icon, color: iconColor, size: 20, ), @@ -213,15 +228,21 @@ Widget buildHistoryTab(BuildContext context, Map token, // Contenu Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ // Type de transaction Text( - _getLocalizedTransactionType(transactionType, context), + _getLocalizedTransactionType( + transactionType, context), style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), + fontSize: + 16 + appState.getTextSizeOffset(), fontWeight: FontWeight.w400, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, ), ), const SizedBox(height: 2), @@ -229,10 +250,12 @@ Widget buildHistoryTab(BuildContext context, Map token, Row( children: [ Text( - price, + price, style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor, + fontSize: 14 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .primaryColor, fontWeight: FontWeight.w500, ), ), @@ -240,8 +263,12 @@ Widget buildHistoryTab(BuildContext context, Map token, Text( "${S.of(context).quantity}: $amount", style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color, + fontSize: 14 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), ), ], @@ -249,10 +276,16 @@ Widget buildHistoryTab(BuildContext context, Map token, const SizedBox(height: 2), // Date Text( - DateFormat('dd MMMM yyyy, HH:mm').format(DateTime.parse(dateTime)), + DateFormat('dd MMMM yyyy, HH:mm') + .format(DateTime.parse(dateTime)), style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), + fontSize: + 12 + appState.getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), ), ), ], @@ -311,13 +344,14 @@ Widget buildHistoryTab(BuildContext context, Map token, ...tokenChanges.map((historyEntry) { final date = DateTime.parse(historyEntry['date']); final changes = historyEntry['changes'] as List? ?? []; - + if (changes.isEmpty) { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 4.0), child: Container( margin: const EdgeInsets.only(bottom: 8.0), decoration: BoxDecoration( @@ -325,7 +359,7 @@ Widget buildHistoryTab(BuildContext context, Map token, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2), ), @@ -351,7 +385,7 @@ Widget buildHistoryTab(BuildContext context, Map token, final changeType = _getChangeTypeFromChange(change); final changeColor = _getChangeColor(changeType); final changeIcon = _getChangeIcon(changeType); - + return Padding( padding: const EdgeInsets.only(bottom: 6.0), child: Row( @@ -361,7 +395,7 @@ Widget buildHistoryTab(BuildContext context, Map token, width: 24, height: 24, decoration: BoxDecoration( - color: changeColor.withOpacity(0.1), + color: changeColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( @@ -374,22 +408,33 @@ Widget buildHistoryTab(BuildContext context, Map token, // Description du changement Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( _getFieldDisplayName(change['field']), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), + fontSize: 13 + + appState.getTextSizeOffset(), fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, ), ), const SizedBox(height: 2), Text( - _formatChangeDescription(change, currencyUtils), + _formatChangeDescription( + change, currencyUtils), style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8), + fontSize: 12 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.8), ), ), ], @@ -398,13 +443,13 @@ Widget buildHistoryTab(BuildContext context, Map token, ], ), ); - }).toList(), + }), ], ), ), ), ); - }).toList() + }) else Padding( padding: const EdgeInsets.all(16.0), @@ -438,7 +483,8 @@ Widget buildHistoryTab(BuildContext context, Map token, } // Widget pour construire une section, comme dans property_tab.dart -Widget _buildSectionCard(BuildContext context, {required String title, required List children}) { +Widget _buildSectionCard(BuildContext context, + {required String title, required List children}) { return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( @@ -446,7 +492,7 @@ Widget _buildSectionCard(BuildContext context, {required String title, required borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -473,7 +519,8 @@ Widget _buildSectionCard(BuildContext context, {required String title, required } // Méthode pour traduire les constantes en textes localisés -String _getLocalizedTransactionType(String transactionType, BuildContext context) { +String _getLocalizedTransactionType( + String transactionType, BuildContext context) { if (transactionType == DataManager.transactionTypePurchase) { return S.of(context).purchase; } else if (transactionType == DataManager.transactionTypeTransfer) { @@ -503,15 +550,15 @@ String _formatHistoryDate(DateTime date) { String _getChangeTypeFromChange(dynamic change) { final oldValue = change['oldValue']; final newValue = change['newValue']; - + if (oldValue == null || newValue == null) { return 'neutral'; } - + // Convertir en double pour la comparaison double? oldVal = double.tryParse(oldValue.toString()); double? newVal = double.tryParse(newValue.toString()); - + if (oldVal != null && newVal != null) { if (newVal > oldVal) { return 'increase'; @@ -519,42 +566,49 @@ String _getChangeTypeFromChange(dynamic change) { return 'decrease'; } } - + return 'neutral'; } -String _formatChangeDescription(dynamic change, CurrencyProvider currencyUtils) { +String _formatChangeDescription( + dynamic change, CurrencyProvider currencyUtils) { final field = change['field']; final oldValue = change['oldValue']; final newValue = change['newValue']; - + if (oldValue == null || newValue == null) { return 'Valeur modifiée'; } - + String formattedOldValue = oldValue.toString(); String formattedNewValue = newValue.toString(); - + // Formatage spécifique selon le champ if (field == 'token_price' || field == 'underlying_asset_price') { double? oldVal = double.tryParse(oldValue.toString()); double? newVal = double.tryParse(newValue.toString()); if (oldVal != null && newVal != null) { - formattedOldValue = '${currencyUtils.convert(oldVal).toStringAsFixed(2)} ${currencyUtils.currencySymbol}'; - formattedNewValue = '${currencyUtils.convert(newVal).toStringAsFixed(2)} ${currencyUtils.currencySymbol}'; + formattedOldValue = + '${currencyUtils.convert(oldVal).toStringAsFixed(2)} ${currencyUtils.currencySymbol}'; + formattedNewValue = + '${currencyUtils.convert(newVal).toStringAsFixed(2)} ${currencyUtils.currencySymbol}'; } - } else if (field == 'total_investment' || field == 'gross_rent_year' || field == 'net_rent_year') { + } else if (field == 'total_investment' || + field == 'gross_rent_year' || + field == 'net_rent_year') { double? oldVal = double.tryParse(oldValue.toString()); double? newVal = double.tryParse(newValue.toString()); if (oldVal != null && newVal != null) { - formattedOldValue = '${currencyUtils.convert(oldVal).toStringAsFixed(0)} ${currencyUtils.currencySymbol}'; - formattedNewValue = '${currencyUtils.convert(newVal).toStringAsFixed(0)} ${currencyUtils.currencySymbol}'; + formattedOldValue = + '${currencyUtils.convert(oldVal).toStringAsFixed(0)} ${currencyUtils.currencySymbol}'; + formattedNewValue = + '${currencyUtils.convert(newVal).toStringAsFixed(0)} ${currencyUtils.currencySymbol}'; } } else if (field == 'rented_units') { - formattedOldValue = '${oldValue} unité${oldValue != '1' ? 's' : ''}'; - formattedNewValue = '${newValue} unité${newValue != '1' ? 's' : ''}'; + formattedOldValue = '$oldValue unité${oldValue != '1' ? 's' : ''}'; + formattedNewValue = '$newValue unité${newValue != '1' ? 's' : ''}'; } - + return '$formattedOldValue → $formattedNewValue'; } @@ -601,13 +655,10 @@ IconData _getChangeIcon(String changeType) { } } -List> _detectTokenChanges( - Map previous, - Map current, - Map tokenInfo -) { +List> _detectTokenChanges(Map previous, + Map current, Map tokenInfo) { List> changes = []; - + // Champs à surveiller final fieldsToWatch = { 'token_price': 'Prix du token', @@ -617,11 +668,11 @@ List> _detectTokenChanges( 'net_rent_year': 'Loyer net annuel', 'rented_units': 'Unités louées', }; - + fieldsToWatch.forEach((field, label) { var prevValue = previous[field]; var currValue = current[field]; - + if (prevValue != null && currValue != null && prevValue != currValue) { changes.add({ 'field': field, @@ -631,6 +682,6 @@ List> _detectTokenChanges( }); } }); - + return changes; } diff --git a/lib/modals/token_details/tabs/insights_tab.dart b/lib/modals/token_details/tabs/insights_tab.dart index c72384e..c2dbcb3 100644 --- a/lib/modals/token_details/tabs/insights_tab.dart +++ b/lib/modals/token_details/tabs/insights_tab.dart @@ -47,7 +47,8 @@ Widget buildInsightsTab(BuildContext context, Map token) { }, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), child: _buildGaugeForROI( token['totalRentReceived'] / token['initialTotalValue'] * 100, context, @@ -64,8 +65,13 @@ Widget buildInsightsTab(BuildContext context, Map token) { title: S.of(context).yieldEvolution, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: _buildYieldChartOrMessage(context, token['historic']?['yields'] ?? [], token['historic']?['init_yield'], currencyUtils), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: _buildYieldChartOrMessage( + context, + token['historic']?['yields'] ?? [], + token['historic']?['init_yield'], + currencyUtils), ), ], ), @@ -78,8 +84,13 @@ Widget buildInsightsTab(BuildContext context, Map token) { title: S.of(context).priceEvolution, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: _buildPriceChartOrMessage(context, token['historic']?['prices'] ?? [], token['initPrice'], currencyUtils), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: _buildPriceChartOrMessage( + context, + token['historic']?['prices'] ?? [], + token['initPrice'], + currencyUtils), ), ], ), @@ -92,8 +103,10 @@ Widget buildInsightsTab(BuildContext context, Map token) { title: S.of(context).cumulativeRentGraph, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: _buildRentCumulativeChartOrMessage(context, token['uuid'], dataManager, currencyUtils), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: _buildRentCumulativeChartOrMessage( + context, token['uuid'], dataManager, currencyUtils), ), ], ), @@ -116,7 +129,7 @@ Widget _buildSectionCard( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -132,7 +145,8 @@ Widget _buildSectionCard( Text( title, style: TextStyle( - fontSize: 18 + Provider.of(context).getTextSizeOffset(), + fontSize: + 18 + Provider.of(context).getTextSizeOffset(), fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor, ), @@ -179,7 +193,8 @@ Widget _buildGaugeForROI(double roiValue, BuildContext context) { height: 14, width: maxWidth, decoration: BoxDecoration( - color: const Color.fromARGB(255, 78, 78, 78).withOpacity(0.2), + color: const Color.fromARGB(255, 78, 78, 78) + .withValues(alpha: 0.2), borderRadius: BorderRadius.circular(7), ), ), @@ -214,7 +229,11 @@ Widget _buildGaugeForROI(double roiValue, BuildContext context) { "Retour sur investissement basé sur les loyers perçus", style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.8), ), ), ], @@ -222,7 +241,8 @@ Widget _buildGaugeForROI(double roiValue, BuildContext context) { } // Méthode pour afficher soit le graphique du yield, soit un message -Widget _buildYieldChartOrMessage(BuildContext context, List yields, double? initYield, CurrencyProvider currencyUtils) { +Widget _buildYieldChartOrMessage(BuildContext context, List yields, + double? initYield, CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); if (yields.length <= 1) { @@ -236,7 +256,9 @@ Widget _buildYieldChartOrMessage(BuildContext context, List yields, dou ), children: [ TextSpan( - text: yields.isNotEmpty ? yields.first['yield'].toStringAsFixed(2) : S.of(context).notSpecified, + text: yields.isNotEmpty + ? yields.first['yield'].toStringAsFixed(2) + : S.of(context).notSpecified, style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color, @@ -251,7 +273,9 @@ Widget _buildYieldChartOrMessage(BuildContext context, List yields, dou } else { // Calculer l'évolution en pourcentage double lastYield = yields.last['yield']?.toDouble() ?? 0; - double percentageChange = ((lastYield - (initYield ?? lastYield)) / (initYield ?? lastYield)) * 100; + double percentageChange = + ((lastYield - (initYield ?? lastYield)) / (initYield ?? lastYield)) * + 100; // Afficher le graphique et le % d'évolution return Column( @@ -286,7 +310,8 @@ Widget _buildYieldChartOrMessage(BuildContext context, List yields, dou } // Méthode pour construire le graphique du yield -Widget _buildYieldChart(BuildContext context, List yields, CurrencyProvider currencyUtils) { +Widget _buildYieldChart(BuildContext context, List yields, + CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); List spots = []; @@ -296,7 +321,9 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv if (yields[i]['timsync'] != null && yields[i]['timsync'] is String) { DateTime date = DateTime.parse(yields[i]['timsync']); double x = i.toDouble(); - double y = yields[i]['yield'] != null ? double.tryParse(yields[i]['yield'].toString()) ?? 0 : 0; + double y = yields[i]['yield'] != null + ? double.tryParse(yields[i]['yield'].toString()) ?? 0 + : 0; y = double.parse(y.toStringAsFixed(2)); spots.add(FlSpot(x, y)); @@ -307,8 +334,12 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv // Calcul des marges double minXValue = spots.isNotEmpty ? spots.first.x : 0; double maxXValue = spots.isNotEmpty ? spots.last.x : 0; - double minYValue = spots.isNotEmpty ? spots.map((spot) => spot.y).reduce((a, b) => a < b ? a : b) : 0; - double maxYValue = spots.isNotEmpty ? spots.map((spot) => spot.y).reduce((a, b) => a > b ? a : b) : 0; + double minYValue = spots.isNotEmpty + ? spots.map((spot) => spot.y).reduce((a, b) => a < b ? a : b) + : 0; + double maxYValue = spots.isNotEmpty + ? spots.map((spot) => spot.y).reduce((a, b) => a > b ? a : b) + : 0; // Ajouter des marges autour des valeurs min et max const double marginX = 0.2; @@ -322,11 +353,11 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv drawHorizontalLine: true, horizontalInterval: 1, getDrawingHorizontalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), getDrawingVerticalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), ), @@ -339,7 +370,8 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < dateLabels.length) { // N'afficher que quelques étiquettes pour éviter l'encombrement - if (dateLabels.length <= 4 || value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { + if (dateLabels.length <= 4 || + value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { return Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( @@ -373,7 +405,8 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv interval: 1, ), ), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), minX: minXValue - marginX, maxX: maxXValue + marginX, @@ -388,7 +421,8 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv isStrokeCapRound: true, dotData: FlDotData( show: true, - getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( radius: 3, color: Theme.of(context).primaryColor, strokeWidth: 1, @@ -397,7 +431,7 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv ), belowBarData: BarAreaData( show: true, - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), ), ), ], @@ -408,7 +442,10 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv final int index = touchedSpot.x.toInt(); return LineTooltipItem( '${dateLabels[index]}: ${touchedSpot.y.toStringAsFixed(2)}%', - TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 + appState.getTextSizeOffset()), + TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12 + appState.getTextSizeOffset()), ); }).toList(); }, @@ -426,7 +463,8 @@ Widget _buildYieldChart(BuildContext context, List yields, CurrencyProv } // Méthode pour afficher soit le graphique des prix, soit un message -Widget _buildPriceChartOrMessage(BuildContext context, List prices, double? initPrice, CurrencyProvider currencyUtils) { +Widget _buildPriceChartOrMessage(BuildContext context, List prices, + double? initPrice, CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); if (prices.length <= 1) { @@ -440,7 +478,11 @@ Widget _buildPriceChartOrMessage(BuildContext context, List prices, dou ), children: [ TextSpan( - text: prices.isNotEmpty ? currencyUtils.formatCurrency(currencyUtils.convert(prices.first['price']), currencyUtils.currencySymbol) : S.of(context).notSpecified, + text: prices.isNotEmpty + ? currencyUtils.formatCurrency( + currencyUtils.convert(prices.first['price']), + currencyUtils.currencySymbol) + : S.of(context).notSpecified, style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color, @@ -452,7 +494,9 @@ Widget _buildPriceChartOrMessage(BuildContext context, List prices, dou } else { // Calculer l'évolution en pourcentage double lastPrice = prices.last['price']?.toDouble() ?? 0; - double percentageChange = ((lastPrice - (initPrice ?? lastPrice)) / (initPrice ?? lastPrice)) * 100; + double percentageChange = + ((lastPrice - (initPrice ?? lastPrice)) / (initPrice ?? lastPrice)) * + 100; // Afficher le graphique et le % d'évolution return Column( @@ -493,7 +537,9 @@ Widget _buildPriceChartOrMessage(BuildContext context, List prices, dou ), children: [ TextSpan( - text: currencyUtils.formatCurrency(currencyUtils.convert(lastPrice), currencyUtils.currencySymbol), + text: currencyUtils.formatCurrency( + currencyUtils.convert(lastPrice), + currencyUtils.currencySymbol), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor, @@ -510,7 +556,8 @@ Widget _buildPriceChartOrMessage(BuildContext context, List prices, dou } // Méthode pour construire le graphique des prix -Widget _buildPriceChart(BuildContext context, List prices, CurrencyProvider currencyUtils) { +Widget _buildPriceChart(BuildContext context, List prices, + CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); List spots = []; @@ -528,8 +575,12 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv // Calcul des marges double minXValue = spots.isNotEmpty ? spots.first.x : 0; double maxXValue = spots.isNotEmpty ? spots.last.x : 0; - double minYValue = spots.isNotEmpty ? spots.map((spot) => spot.y).reduce((a, b) => a < b ? a : b) : 0; - double maxYValue = spots.isNotEmpty ? spots.map((spot) => spot.y).reduce((a, b) => a > b ? a : b) : 0; + double minYValue = spots.isNotEmpty + ? spots.map((spot) => spot.y).reduce((a, b) => a < b ? a : b) + : 0; + double maxYValue = spots.isNotEmpty + ? spots.map((spot) => spot.y).reduce((a, b) => a > b ? a : b) + : 0; // Ajouter des marges autour des valeurs min et max const double marginX = 0.1; @@ -542,11 +593,11 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv drawVerticalLine: true, drawHorizontalLine: true, getDrawingHorizontalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), getDrawingVerticalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), ), @@ -559,7 +610,8 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < dateLabels.length) { // N'afficher que quelques étiquettes pour éviter l'encombrement - if (dateLabels.length <= 4 || value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { + if (dateLabels.length <= 4 || + value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { return Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( @@ -594,7 +646,8 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv }, ), ), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), minX: minXValue - marginX, maxX: maxXValue + marginX, @@ -609,7 +662,8 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv isStrokeCapRound: true, dotData: FlDotData( show: true, - getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( radius: 3, color: Theme.of(context).primaryColor, strokeWidth: 1, @@ -618,7 +672,7 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv ), belowBarData: BarAreaData( show: true, - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), ), ), ], @@ -630,7 +684,10 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv final convertedValue = currencyUtils.convert(touchedSpot.y); return LineTooltipItem( '${dateLabels[index]}: ${currencyUtils.formatCurrency(convertedValue, currencyUtils.currencySymbol)}', - TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 + appState.getTextSizeOffset()), + TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12 + appState.getTextSizeOffset()), ); }).toList(); }, @@ -648,11 +705,13 @@ Widget _buildPriceChart(BuildContext context, List prices, CurrencyProv } // Méthode pour afficher soit le graphique cumulatif des loyers, soit un message -Widget _buildRentCumulativeChartOrMessage(BuildContext context, String tokenId, DataManager dataManager, CurrencyProvider currencyUtils) { +Widget _buildRentCumulativeChartOrMessage(BuildContext context, String tokenId, + DataManager dataManager, CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); // Récupérer l'historique des loyers pour ce token - List> rentHistory = dataManager.getRentHistoryForToken(tokenId); + List> rentHistory = + dataManager.getRentHistoryForToken(tokenId); // Récupérer le nombre de wallets qui possèdent ce token int walletCount = dataManager.getWalletCountForToken(tokenId); @@ -668,7 +727,8 @@ Widget _buildRentCumulativeChartOrMessage(BuildContext context, String tokenId, ); } else { // Calculer le montant total des loyers - double totalRent = dataManager.cumulativeRentsByToken[tokenId.toLowerCase()] ?? 0.0; + double totalRent = + dataManager.cumulativeRentsByToken[tokenId.toLowerCase()] ?? 0.0; // Afficher le graphique et le montant total return Column( @@ -698,7 +758,10 @@ Widget _buildRentCumulativeChartOrMessage(BuildContext context, String tokenId, ), children: [ TextSpan( - text: currencyUtils.getFormattedAmount(currencyUtils.convert(totalRent), currencyUtils.currencySymbol, appState.showAmounts), + text: currencyUtils.getFormattedAmount( + currencyUtils.convert(totalRent), + currencyUtils.currencySymbol, + appState.showAmounts), style: TextStyle( fontWeight: FontWeight.bold, color: Colors.green[700], @@ -715,7 +778,8 @@ Widget _buildRentCumulativeChartOrMessage(BuildContext context, String tokenId, } // Méthode pour construire le graphique cumulatif des loyers -Widget _buildRentCumulativeChart(BuildContext context, List> rentHistory, CurrencyProvider currencyUtils) { +Widget _buildRentCumulativeChart(BuildContext context, + List> rentHistory, CurrencyProvider currencyUtils) { final appState = Provider.of(context, listen: false); // Trier l'historique par date @@ -769,7 +833,9 @@ Widget _buildRentCumulativeChart(BuildContext context, List // Calcul des marges double minXValue = spots.isNotEmpty ? spots.first.x : 0; double maxXValue = spots.isNotEmpty ? spots.last.x : 0; - double maxYValue = spots.isNotEmpty ? spots.last.y : 0; // La dernière valeur est la plus élevée pour un cumul + double maxYValue = spots.isNotEmpty + ? spots.last.y + : 0; // La dernière valeur est la plus élevée pour un cumul // Ajouter des marges autour des valeurs min et max const double marginX = 0.2; @@ -782,11 +848,11 @@ Widget _buildRentCumulativeChart(BuildContext context, List drawVerticalLine: true, drawHorizontalLine: true, getDrawingHorizontalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), getDrawingVerticalLine: (value) => FlLine( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), strokeWidth: 0.5, ), ), @@ -799,7 +865,8 @@ Widget _buildRentCumulativeChart(BuildContext context, List getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < dateLabels.length) { // N'afficher que quelques dates pour éviter la surcharge - if (dateLabels.length <= 4 || value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { + if (dateLabels.length <= 4 || + value.toInt() % (dateLabels.length ~/ 4 + 1) == 0) { return Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( @@ -833,11 +900,13 @@ Widget _buildRentCumulativeChart(BuildContext context, List }, ), ), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), minX: minXValue - marginX, maxX: maxXValue + marginX, - minY: 0 - marginY / 2, // Le minimum est toujours 0 pour un graphique cumulatif + minY: 0 - + marginY / 2, // Le minimum est toujours 0 pour un graphique cumulatif maxY: maxYValue + marginY, lineBarsData: [ LineChartBarData( @@ -849,7 +918,7 @@ Widget _buildRentCumulativeChart(BuildContext context, List dotData: FlDotData(show: false), belowBarData: BarAreaData( show: true, - color: Colors.green.withOpacity(0.1), + color: Colors.green.withValues(alpha: 0.1), ), ), ], @@ -861,7 +930,10 @@ Widget _buildRentCumulativeChart(BuildContext context, List final convertedValue = currencyUtils.convert(touchedSpot.y); return LineTooltipItem( '${dateLabels[index]}: ${currencyUtils.formatCurrency(convertedValue, currencyUtils.currencySymbol)}', - TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 + appState.getTextSizeOffset()), + TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12 + appState.getTextSizeOffset()), ); }).toList(); }, diff --git a/lib/modals/token_details/tabs/market_tab.dart b/lib/modals/token_details/tabs/market_tab.dart index 219eb17..79c8645 100644 --- a/lib/modals/token_details/tabs/market_tab.dart +++ b/lib/modals/token_details/tabs/market_tab.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; @@ -25,7 +23,8 @@ class _MarketTabState extends State { String selectedOfferType = "tout"; String selectedSortOption = "delta"; bool ascending = true; - bool isTabBarSticky = false; // Nouvel état pour suivre si la TabBar est sticky + bool isTabBarSticky = + false; // Nouvel état pour suivre si la TabBar est sticky // Définition des couleurs iOS final Color _iosPrimaryColor = const Color(0xFF007AFF); // Bleu iOS @@ -51,7 +50,8 @@ class _MarketTabState extends State { void _setupScrollListener() { // Trouver le contrôleur de défilement parent - final ancestor = context.findAncestorWidgetOfExactType(); + final ancestor = + context.findAncestorWidgetOfExactType(); if (ancestor != null) { setState(() { _parentScrollController = ancestor.scrollController; @@ -89,11 +89,13 @@ class _MarketTabState extends State { final currencyUtils = Provider.of(context, listen: false); // Utilisation des couleurs du thème si disponibles - final isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark; + final isDarkMode = + MediaQuery.of(context).platformBrightness == Brightness.dark; // Si c'est une modale plein écran séparée if (widget.isModal) { - return _buildModalContent(appState, dataManager, currencyUtils, isDarkMode); + return _buildModalContent( + appState, dataManager, currencyUtils, isDarkMode); } // Si c'est intégré dans la page de détails du token @@ -108,14 +110,16 @@ class _MarketTabState extends State { // Contenu principal avec liste défilante qui s'active quand la TabBar est sticky Expanded( child: FutureBuilder>>( - future: _getFilteredOffers(dataManager, widget.token['uuid'], selectedOfferType), + future: _getFilteredOffers( + dataManager, widget.token['uuid'], selectedOfferType), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CupertinoActivityIndicator(), ); } else if (snapshot.hasError) { - return _buildErrorWidget(snapshot.error.toString(), isDarkMode); + return _buildErrorWidget( + snapshot.error.toString(), isDarkMode); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return _buildEmptyWidget(appState, isDarkMode); } else { @@ -130,7 +134,8 @@ class _MarketTabState extends State { children: [ // Titre de la section Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0, bottom: 8.0), + padding: const EdgeInsets.only( + left: 8.0, right: 8.0, top: 16.0, bottom: 8.0), child: Text( S.of(context).secondary_offers_related_to_token, style: TextStyle( @@ -143,18 +148,38 @@ class _MarketTabState extends State { // Construction de toutes les offres dans le ListView ...offers.map((offer) { - final bool isTokenWhitelisted = dataManager.whitelistTokens.any( - (whitelisted) => whitelisted['token'].toLowerCase() == widget.token['uuid'].toLowerCase(), + final bool isTokenWhitelisted = + dataManager.whitelistTokens.any( + (whitelisted) => + whitelisted['token'].toLowerCase() == + widget.token['uuid'].toLowerCase(), ); - if (selectedOfferType == "vente" || (selectedOfferType == "tout" && offer['token_to_buy'] == null)) { - return _buildIOSSaleOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); - } else if (selectedOfferType == "achat" || (selectedOfferType == "tout" && offer['token_to_buy'] != null)) { - return _buildIOSPurchaseOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); + if (selectedOfferType == "vente" || + (selectedOfferType == "tout" && + offer['token_to_buy'] == null)) { + return _buildIOSSaleOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); + } else if (selectedOfferType == "achat" || + (selectedOfferType == "tout" && + offer['token_to_buy'] != null)) { + return _buildIOSPurchaseOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); } else { - return const SizedBox.shrink(); // Ne devrait jamais arriver + return const SizedBox + .shrink(); // Ne devrait jamais arriver } - }).toList(), + }), // Espace en bas const SizedBox(height: 16), @@ -170,7 +195,8 @@ class _MarketTabState extends State { } // Contenu pour l'affichage modal plein écran - Widget _buildModalContent(AppState appState, DataManager dataManager, CurrencyProvider currencyUtils, bool isDarkMode) { + Widget _buildModalContent(AppState appState, DataManager dataManager, + CurrencyProvider currencyUtils, bool isDarkMode) { return Column( children: [ // Header pour la vue modale - fixe @@ -180,7 +206,7 @@ class _MarketTabState extends State { color: isDarkMode ? Colors.black : _iosSurfaceColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 1), ), @@ -221,7 +247,8 @@ class _MarketTabState extends State { children: [ // Titre de la section Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 20, bottom: 10), + padding: const EdgeInsets.only( + left: 16, right: 16, top: 20, bottom: 10), child: Text( S.of(context).secondary_offers_related_to_token, style: TextStyle( @@ -235,7 +262,8 @@ class _MarketTabState extends State { // Liste des offres FutureBuilder>>( - future: _getFilteredOffers(dataManager, widget.token['uuid'], selectedOfferType), + future: _getFilteredOffers( + dataManager, widget.token['uuid'], selectedOfferType), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( @@ -245,7 +273,8 @@ class _MarketTabState extends State { ), ); } else if (snapshot.hasError) { - return _buildErrorWidget(snapshot.error.toString(), isDarkMode); + return _buildErrorWidget( + snapshot.error.toString(), isDarkMode); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return _buildEmptyWidget(appState, isDarkMode); } else { @@ -256,20 +285,47 @@ class _MarketTabState extends State { shrinkWrap: true, itemBuilder: (context, index) { final offer = offers[index]; - final bool isTokenWhitelisted = dataManager.whitelistTokens.any( - (whitelisted) => whitelisted['token'].toLowerCase() == widget.token['uuid'].toLowerCase(), + final bool isTokenWhitelisted = + dataManager.whitelistTokens.any( + (whitelisted) => + whitelisted['token'].toLowerCase() == + widget.token['uuid'].toLowerCase(), ); // Render appropriate card based on offer type if (selectedOfferType == "vente") { - return _buildIOSSaleOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); + return _buildIOSSaleOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); } else if (selectedOfferType == "achat") { - return _buildIOSPurchaseOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); + return _buildIOSPurchaseOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); } else { if (offer['token_to_buy'] == null) { - return _buildIOSSaleOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); + return _buildIOSSaleOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); } else { - return _buildIOSPurchaseOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted, isDarkMode); + return _buildIOSPurchaseOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted, + isDarkMode); } } }, @@ -297,14 +353,14 @@ class _MarketTabState extends State { color: isDarkMode ? const Color(0xFF1C1C1E) : _iosSurfaceColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], border: Border( bottom: BorderSide( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), width: 0.5, ), ), @@ -321,7 +377,8 @@ class _MarketTabState extends State { Text( S.of(context).sort_label, style: TextStyle( - fontSize: 13 + Provider.of(context).getTextSizeOffset(), + fontSize: + 13 + Provider.of(context).getTextSizeOffset(), color: isDarkMode ? Colors.white70 : _iosSecondaryLabelColor, ), ), @@ -331,7 +388,9 @@ class _MarketTabState extends State { padding: EdgeInsets.zero, minSize: 0, child: Icon( - ascending ? CupertinoIcons.arrow_up : CupertinoIcons.arrow_down, + ascending + ? CupertinoIcons.arrow_up + : CupertinoIcons.arrow_down, size: 18, color: isDarkMode ? Colors.white : _iosPrimaryColor, ), @@ -372,7 +431,8 @@ class _MarketTabState extends State { topLeft: Radius.circular(16), topRight: Radius.circular(16), ), - child: MarketTab(token: widget.token, isModal: true), + child: + MarketTab(token: widget.token, isModal: true), ), ); }, @@ -437,7 +497,8 @@ class _MarketTabState extends State { } // Tri des offres - List> _getSortedOffers(List> offers) { + List> _getSortedOffers( + List> offers) { offers.sort((a, b) { if (selectedSortOption == "date") { final dateA = a['creationDate']; @@ -458,22 +519,27 @@ class _MarketTabState extends State { scrollDirection: Axis.horizontal, child: Row( children: [ - _buildFilterChip("tout", CupertinoIcons.rectangle_grid_1x2_fill, selectedOfferType == "tout", context), + _buildFilterChip("tout", CupertinoIcons.rectangle_grid_1x2_fill, + selectedOfferType == "tout", context), const SizedBox(width: 8), - _buildFilterChip("vente", CupertinoIcons.shopping_cart, selectedOfferType == "vente", context), + _buildFilterChip("vente", CupertinoIcons.shopping_cart, + selectedOfferType == "vente", context), const SizedBox(width: 8), - _buildFilterChip("achat", CupertinoIcons.tag_fill, selectedOfferType == "achat", context), + _buildFilterChip("achat", CupertinoIcons.tag_fill, + selectedOfferType == "achat", context), ], ), ); } // Widget pour créer un filtre chip style iOS - Widget _buildFilterChip(String type, IconData icon, bool isSelected, BuildContext context, {VoidCallback? onTap, Color? customColor}) { + Widget _buildFilterChip( + String type, IconData icon, bool isSelected, BuildContext context, + {VoidCallback? onTap, Color? customColor}) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final color = customColor ?? (isSelected - ? Theme.of(context).primaryColor.withOpacity(0.2) + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) : isDarkMode ? const Color(0xFF1C1C1E) : Colors.white); @@ -491,12 +557,14 @@ class _MarketTabState extends State { color: color, borderRadius: BorderRadius.circular(16), border: Border.all( - color: isSelected ? Theme.of(context).primaryColor : Colors.transparent, + color: isSelected + ? Theme.of(context).primaryColor + : Colors.transparent, width: 1, ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2), ), @@ -508,7 +576,9 @@ class _MarketTabState extends State { Icon( icon, size: 16, - color: isSelected ? Theme.of(context).primaryColor : Colors.grey[600], + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey[600], ), ], ), @@ -525,7 +595,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: const Offset(0, 1), ), @@ -536,14 +606,16 @@ class _MarketTabState extends State { value: selectedSortOption == "delta" ? "delta" : "date", underline: const SizedBox(), isDense: true, - icon: Icon(Icons.keyboard_arrow_down, color: Theme.of(context).primaryColor, size: 18), + icon: Icon(Icons.keyboard_arrow_down, + color: Theme.of(context).primaryColor, size: 18), items: [ DropdownMenuItem( value: "delta", child: Text( S.of(context).sort_delta, style: TextStyle( - fontSize: 13 + Provider.of(context).getTextSizeOffset(), + fontSize: + 13 + Provider.of(context).getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -553,7 +625,8 @@ class _MarketTabState extends State { child: Text( S.of(context).sort_date, style: TextStyle( - fontSize: 13 + Provider.of(context).getTextSizeOffset(), + fontSize: + 13 + Provider.of(context).getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -569,13 +642,20 @@ class _MarketTabState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.arrow_upward, size: 14, color: ascending ? Theme.of(context).primaryColor : Colors.grey), + Icon(Icons.arrow_upward, + size: 14, + color: ascending + ? Theme.of(context).primaryColor + : Colors.grey), const SizedBox(width: 3), Text( "Ascendant", style: TextStyle( - fontSize: 13 + Provider.of(context).getTextSizeOffset(), - color: ascending ? Theme.of(context).primaryColor : Colors.grey, + fontSize: + 13 + Provider.of(context).getTextSizeOffset(), + color: ascending + ? Theme.of(context).primaryColor + : Colors.grey, ), ), ], @@ -586,13 +666,20 @@ class _MarketTabState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.arrow_downward, size: 14, color: !ascending ? Theme.of(context).primaryColor : Colors.grey), + Icon(Icons.arrow_downward, + size: 14, + color: !ascending + ? Theme.of(context).primaryColor + : Colors.grey), const SizedBox(width: 3), Text( "Descendant", style: TextStyle( - fontSize: 13 + Provider.of(context).getTextSizeOffset(), - color: !ascending ? Theme.of(context).primaryColor : Colors.grey, + fontSize: + 13 + Provider.of(context).getTextSizeOffset(), + color: !ascending + ? Theme.of(context).primaryColor + : Colors.grey, ), ), ], @@ -625,8 +712,10 @@ class _MarketTabState extends State { bool isTokenWhitelisted, bool isDarkMode, ) { - final baseYield = double.tryParse(widget.token['annualPercentageYield'].toString()) ?? 0; - final initialPrice = double.tryParse(widget.token['initPrice'].toString()) ?? 0; + final baseYield = + double.tryParse(widget.token['annualPercentageYield'].toString()) ?? 0; + final initialPrice = + double.tryParse(widget.token['initPrice'].toString()) ?? 0; final offerPrice = double.tryParse(offer['token_value'].toString()) ?? 0; if (baseYield <= 0 || initialPrice <= 0 || offerPrice <= 0) { @@ -634,10 +723,12 @@ class _MarketTabState extends State { } final newYield = baseYield * (initialPrice / offerPrice); - final premiumPercentage = ((offerPrice - initialPrice) / initialPrice) * 100; + final premiumPercentage = + ((offerPrice - initialPrice) / initialPrice) * 100; final roiWeeks = (premiumPercentage * 52) / baseYield; - final double deltaValue = ((offer['token_value'] / offer['token_price'] - 1) * 100); + final double deltaValue = + ((offer['token_value'] / offer['token_price'] - 1) * 100); // Détermination de la couleur selon le delta Color deltaColor; @@ -659,7 +750,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6, offset: const Offset(0, 1), ), @@ -675,12 +766,15 @@ class _MarketTabState extends State { // Warning si non whitelisté if (!isTokenWhitelisted) Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 4), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( - color: _iosDangerColor.withOpacity(0.1), + color: _iosDangerColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), - border: Border.all(color: _iosDangerColor.withOpacity(0.3), width: 0.5), + border: Border.all( + color: _iosDangerColor.withValues(alpha: 0.3), + width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -711,7 +805,9 @@ class _MarketTabState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -731,7 +827,8 @@ class _MarketTabState extends State { ), ), Text( - CustomDateUtils.formatReadableDate(offer['creationDate']), + CustomDateUtils.formatReadableDate( + offer['creationDate']), style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey[500], @@ -751,9 +848,12 @@ class _MarketTabState extends State { // Montant du token Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF3A3A3C) : Colors.grey[100], + color: isDarkMode + ? const Color(0xFF3A3A3C) + : Colors.grey[100], borderRadius: BorderRadius.circular(10), ), child: Column( @@ -783,9 +883,10 @@ class _MarketTabState extends State { // Delta price Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: deltaColor.withOpacity(0.1), + color: deltaColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -801,14 +902,17 @@ class _MarketTabState extends State { Row( children: [ Icon( - deltaValue < 0 ? Icons.arrow_downward : Icons.arrow_upward, + deltaValue < 0 + ? Icons.arrow_downward + : Icons.arrow_upward, color: deltaColor, size: 12, ), Text( '${deltaValue.abs().toStringAsFixed(2)}%', style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: + 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, color: deltaColor, ), @@ -830,8 +934,8 @@ class _MarketTabState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.blue.withOpacity(0.1), - deltaColor.withOpacity(0.1), + Colors.blue.withValues(alpha: 0.1), + deltaColor.withValues(alpha: 0.1), ], begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -853,7 +957,8 @@ class _MarketTabState extends State { ), ), Text( - currencyUtils.formatCurrency(initialPrice, currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + initialPrice, currencyUtils.currencySymbol), style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -879,7 +984,8 @@ class _MarketTabState extends State { ), ), Text( - currencyUtils.formatCurrency(offerPrice, currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + offerPrice, currencyUtils.currencySymbol), style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -900,8 +1006,8 @@ class _MarketTabState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.blue.withOpacity(0.1), - deltaColor.withOpacity(0.1), + Colors.blue.withValues(alpha: 0.1), + deltaColor.withValues(alpha: 0.1), ], begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -972,12 +1078,15 @@ class _MarketTabState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( - color: const Color(0xFFFF9500).withOpacity(0.2), // Orange iOS + color: const Color(0xFFFF9500) + .withValues(alpha: 0.2), // Orange iOS borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( - S.of(context).roi_label(roiWeeks.toStringAsFixed(1)), + S + .of(context) + .roi_label(roiWeeks.toStringAsFixed(1)), style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -994,23 +1103,28 @@ class _MarketTabState extends State { Expanded( child: Material( borderRadius: BorderRadius.circular(10), - color: isTokenWhitelisted ? const Color(0xFF007AFF) : Colors.grey, + color: isTokenWhitelisted + ? const Color(0xFF007AFF) + : Colors.grey, child: InkWell( borderRadius: BorderRadius.circular(10), onTap: isTokenWhitelisted ? () { - UrlUtils.launchURL('https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); + UrlUtils.launchURL( + 'https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); } : null, child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: + const EdgeInsets.symmetric(vertical: 8), child: Center( child: Text( S.of(context).buy_token, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 12 + appState.getTextSizeOffset(), + fontSize: + 12 + appState.getTextSizeOffset(), ), ), ), @@ -1031,7 +1145,10 @@ class _MarketTabState extends State { builder: (context) { Widget icon = const SizedBox(); - if (offer['token_to_pay'] == '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || offer['token_to_pay'] == '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { + if (offer['token_to_pay'] == + '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || + offer['token_to_pay'] == + '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { icon = Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( @@ -1039,7 +1156,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -1051,7 +1168,10 @@ class _MarketTabState extends State { height: 18, ), ); - } else if (offer['token_to_pay'] == '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || offer['token_to_pay'] == '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { + } else if (offer['token_to_pay'] == + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || + offer['token_to_pay'] == + '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { icon = Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( @@ -1059,7 +1179,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -1092,7 +1212,8 @@ class _MarketTabState extends State { bool isTokenWhitelisted, bool isDarkMode, ) { - final double deltaValue = ((offer['token_value'] / offer['token_price'] - 1) * 100); + final double deltaValue = + ((offer['token_value'] / offer['token_price'] - 1) * 100); // Définir les couleurs selon le delta Color deltaColor = deltaValue < 0 @@ -1109,7 +1230,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6, offset: const Offset(0, 1), ), @@ -1125,12 +1246,15 @@ class _MarketTabState extends State { // Warning si non whitelisté if (!isTokenWhitelisted) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( - color: _iosDangerColor.withOpacity(0.1), + color: _iosDangerColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), - border: Border.all(color: _iosDangerColor.withOpacity(0.3), width: 0.5), + border: Border.all( + color: _iosDangerColor.withValues(alpha: 0.3), + width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -1161,7 +1285,9 @@ class _MarketTabState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -1181,7 +1307,8 @@ class _MarketTabState extends State { ), ), Text( - CustomDateUtils.formatReadableDate(offer['creationDate']), + CustomDateUtils.formatReadableDate( + offer['creationDate']), style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey[500], @@ -1200,9 +1327,12 @@ class _MarketTabState extends State { // Montant du token Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF3A3A3C) : Colors.grey[100], + color: isDarkMode + ? const Color(0xFF3A3A3C) + : Colors.grey[100], borderRadius: BorderRadius.circular(10), ), child: Column( @@ -1232,9 +1362,12 @@ class _MarketTabState extends State { // Valeur du token Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF3A3A3C) : Colors.grey[100], + color: isDarkMode + ? const Color(0xFF3A3A3C) + : Colors.grey[100], borderRadius: BorderRadius.circular(10), ), child: Column( @@ -1248,7 +1381,10 @@ class _MarketTabState extends State { ), ), Text( - currencyUtils.formatCurrency(currencyUtils.convert(offer['token_value']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils + .convert(offer['token_value']), + currencyUtils.currencySymbol), style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -1267,7 +1403,7 @@ class _MarketTabState extends State { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: deltaColor.withOpacity(0.1), + color: deltaColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Row( @@ -1281,7 +1417,9 @@ class _MarketTabState extends State { ), ), Icon( - deltaValue < 0 ? Icons.arrow_downward : Icons.arrow_upward, + deltaValue < 0 + ? Icons.arrow_downward + : Icons.arrow_upward, color: deltaColor, size: 14, ), @@ -1302,12 +1440,15 @@ class _MarketTabState extends State { // Bouton d'action style iOS Material( borderRadius: BorderRadius.circular(10), - color: isTokenWhitelisted ? _iosSuccessColor : Colors.grey, // Vert iOS + color: isTokenWhitelisted + ? _iosSuccessColor + : Colors.grey, // Vert iOS child: InkWell( borderRadius: BorderRadius.circular(10), onTap: isTokenWhitelisted ? () { - UrlUtils.launchURL('https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); + UrlUtils.launchURL( + 'https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); } : null, child: Container( @@ -1337,7 +1478,10 @@ class _MarketTabState extends State { builder: (context) { Widget icon = const SizedBox(); - if (offer['token_to_pay'] == '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || offer['token_to_pay'] == '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { + if (offer['token_to_pay'] == + '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || + offer['token_to_pay'] == + '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { icon = Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( @@ -1345,7 +1489,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -1357,7 +1501,10 @@ class _MarketTabState extends State { height: 18, ), ); - } else if (offer['token_to_pay'] == '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || offer['token_to_pay'] == '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { + } else if (offer['token_to_pay'] == + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || + offer['token_to_pay'] == + '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { icon = Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( @@ -1365,7 +1512,7 @@ class _MarketTabState extends State { borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -1402,10 +1549,10 @@ class _MarketTabState extends State { return Container( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: isDarkMode ? Colors.black12 : valueColor.withOpacity(0.05), + color: isDarkMode ? Colors.black12 : valueColor.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(10), border: Border.all( - color: valueColor.withOpacity(0.2), + color: valueColor.withValues(alpha: 0.2), width: 0.5, ), ), @@ -1440,8 +1587,10 @@ Future>> _getFilteredOffers( String tokenUuid, String offerType, ) async { - List> filteredOffers = dataManager.yamMarket.where((offer) { - bool matchToken = offer['token_to_sell'] == tokenUuid.toLowerCase() || offer['token_to_buy'] == tokenUuid.toLowerCase(); + List> filteredOffers = + dataManager.yamMarket.where((offer) { + bool matchToken = offer['token_to_sell'] == tokenUuid.toLowerCase() || + offer['token_to_buy'] == tokenUuid.toLowerCase(); if (!matchToken) return false; if (offerType == "vente") { return offer['token_to_buy'] == null; diff --git a/lib/modals/token_details/tabs/others_tab.dart b/lib/modals/token_details/tabs/others_tab.dart index 77358d2..fd5eac9 100644 --- a/lib/modals/token_details/tabs/others_tab.dart +++ b/lib/modals/token_details/tabs/others_tab.dart @@ -48,7 +48,8 @@ Widget buildOthersTab(BuildContext context, Map token) { onTap: () { final ethereumAddress = token['ethereumContract'] ?? ''; if (ethereumAddress.isNotEmpty) { - UrlUtils.launchURL('https://etherscan.io/address/$ethereumAddress'); + UrlUtils.launchURL( + 'https://etherscan.io/address/$ethereumAddress'); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -74,7 +75,8 @@ Widget buildOthersTab(BuildContext context, Map token) { onTap: () { final gnosisAddress = token['gnosisContract'] ?? ''; if (gnosisAddress.isNotEmpty) { - UrlUtils.launchURL('https://gnosisscan.io/address/$gnosisAddress'); + UrlUtils.launchURL( + 'https://gnosisscan.io/address/$gnosisAddress'); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -102,15 +104,21 @@ Widget buildOthersTab(BuildContext context, Map token) { context, icon: isWhitelisted ? Icons.check_circle : Icons.cancel, iconColor: isWhitelisted ? Colors.green : Colors.red, - label: isWhitelisted ? S.of(context).tokenWhitelisted : S.of(context).tokenNotWhitelisted, + label: isWhitelisted + ? S.of(context).tokenWhitelisted + : S.of(context).tokenNotWhitelisted, textColor: isWhitelisted ? Colors.green : Colors.red, ), const Divider(height: 1, thickness: 0.5), _buildStatusRow( context, - icon: isInWallet ? Icons.account_balance_wallet : Icons.account_balance_wallet_outlined, + icon: isInWallet + ? Icons.account_balance_wallet + : Icons.account_balance_wallet_outlined, iconColor: isInWallet ? Colors.green : Colors.red, - label: isInWallet ? S.of(context).presentInWallet : S.of(context).filterNotInWallet, + label: isInWallet + ? S.of(context).presentInWallet + : S.of(context).filterNotInWallet, textColor: isInWallet ? Colors.green : Colors.red, ), ], @@ -131,7 +139,8 @@ Widget buildOthersTab(BuildContext context, Map token) { walletAddress: walletAddress, showFull: appState.showAmounts, ), - if (tokenWallets.last != walletAddress) const Divider(height: 1, thickness: 0.5), + if (tokenWallets.last != walletAddress) + const Divider(height: 1, thickness: 0.5), ], ); }).toList() @@ -156,7 +165,8 @@ Widget buildOthersTab(BuildContext context, Map token) { } // Widget pour construire une section avec titre, comme dans property_tab.dart -Widget _buildSectionCard(BuildContext context, {required String title, required List children}) { +Widget _buildSectionCard(BuildContext context, + {required String title, required List children}) { return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( @@ -164,7 +174,7 @@ Widget _buildSectionCard(BuildContext context, {required String title, required borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -215,7 +225,7 @@ Widget _buildContractRow( height: 32, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Image.asset( @@ -239,7 +249,7 @@ Widget _buildContractRow( icon: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -259,10 +269,10 @@ Widget _buildContractRow( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.05), + color: Colors.grey.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), width: 1, ), ), @@ -299,7 +309,7 @@ Widget _buildStatusRow( width: 32, height: 32, decoration: BoxDecoration( - color: iconColor.withOpacity(0.1), + color: iconColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -338,7 +348,7 @@ Widget _buildWalletRow( width: 32, height: 32, decoration: BoxDecoration( - color: Colors.purple.withOpacity(0.1), + color: Colors.purple.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -366,15 +376,17 @@ Widget _buildWalletRow( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.05), + color: Colors.grey.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), width: 1, ), ), child: Text( - showFull ? walletAddress : TextUtils.truncateWallet(walletAddress), + showFull + ? walletAddress + : TextUtils.truncateWallet(walletAddress), style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), fontFamily: 'Menlo', // Police monospace style iOS diff --git a/lib/modals/token_details/tabs/property_tab.dart b/lib/modals/token_details/tabs/property_tab.dart index 3724736..8d159f4 100644 --- a/lib/modals/token_details/tabs/property_tab.dart +++ b/lib/modals/token_details/tabs/property_tab.dart @@ -7,7 +7,8 @@ import 'package:realtoken_asset_tracker/utils/location_utils.dart'; import 'package:realtoken_asset_tracker/utils/parameters.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; -Widget buildPropertiesTab(BuildContext context, Map token, bool convertToSquareMeters) { +Widget buildPropertiesTab(BuildContext context, Map token, + bool convertToSquareMeters) { final appState = Provider.of(context, listen: false); return Padding( @@ -52,7 +53,8 @@ Widget buildPropertiesTab(BuildContext context, Map token, bool _buildDetailRow( context, S.of(context).constructionYear, - token['constructionYear']?.toString() ?? S.of(context).notSpecified, + token['constructionYear']?.toString() ?? + S.of(context).notSpecified, icon: Icons.calendar_today, iconColor: Colors.blue, ), @@ -60,7 +62,8 @@ Widget buildPropertiesTab(BuildContext context, Map token, bool _buildDetailRow( context, S.of(context).propertyType, - Parameters.getPropertyTypeName(token['propertyType'] ?? -1, context), + Parameters.getPropertyTypeName( + token['propertyType'] ?? -1, context), icon: Icons.home, iconColor: Colors.teal, ), @@ -110,7 +113,8 @@ Widget buildPropertiesTab(BuildContext context, Map token, bool } // Méthode pour construire une section avec carte -Widget _buildSectionCard(BuildContext context, {required String title, required List children}) { +Widget _buildSectionCard(BuildContext context, + {required String title, required List children}) { return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( @@ -118,7 +122,7 @@ Widget _buildSectionCard(BuildContext context, {required String title, required borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -145,7 +149,8 @@ Widget _buildSectionCard(BuildContext context, {required String title, required } // Méthode pour construire les lignes de détails -Widget _buildDetailRow(BuildContext context, String label, String value, {IconData? icon, Color? iconColor}) { +Widget _buildDetailRow(BuildContext context, String label, String value, + {IconData? icon, Color? iconColor}) { final appState = Provider.of(context, listen: false); return Padding( @@ -160,7 +165,7 @@ Widget _buildDetailRow(BuildContext context, String label, String value, {IconDa width: 32, height: 32, decoration: BoxDecoration( - color: (iconColor ?? Colors.blue).withOpacity(0.1), + color: (iconColor ?? Colors.blue).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -194,7 +199,8 @@ Widget _buildDetailRow(BuildContext context, String label, String value, {IconDa } // Méthode spécifique pour la ligne d'état de location -Widget _buildRentalStatusRow(BuildContext context, {required Map token, required AppState appState}) { +Widget _buildRentalStatusRow(BuildContext context, + {required Map token, required AppState appState}) { final rentedUnits = token['rentedUnits'] ?? 0; final totalUnits = token['totalUnits'] ?? 1; final occupancyRate = (rentedUnits / totalUnits * 100).round(); @@ -215,7 +221,7 @@ Widget _buildRentalStatusRow(BuildContext context, {required Map token, required AppState appState}) { +Widget _buildRentStartDateRow(BuildContext context, + {required Map token, required AppState appState}) { final rentStartDate = token['rentStartDate'] ?? ''; final isActive = DateTime.parse(rentStartDate).isBefore(DateTime.now()); final statusColor = isActive ? Colors.green : Colors.red; @@ -309,7 +316,7 @@ Widget _buildRentStartDateRow(BuildContext context, {required Map json) { return RentRecord( - timestamp: json['timestamp'] is String - ? DateTime.parse(json['timestamp']) + timestamp: json['timestamp'] is String + ? DateTime.parse(json['timestamp']) : DateTime.fromMillisecondsSinceEpoch(json['timestamp']), rent: json['rent']?.toDouble() ?? 0.0, cumulativeRent: json['cumulativeRent']?.toDouble() ?? 0.0, @@ -27,4 +27,4 @@ class RentRecord { 'cumulativeRent': cumulativeRent, }; } -} \ No newline at end of file +} diff --git a/lib/models/roi_record.dart b/lib/models/roi_record.dart index 4a86e2f..1957fdf 100644 --- a/lib/models/roi_record.dart +++ b/lib/models/roi_record.dart @@ -1,4 +1,6 @@ /// Modèle pour représenter les données de Retour sur Investissement (ROI) +library; + import 'package:flutter/foundation.dart'; // Pour debugPrint class ROIRecord { @@ -14,7 +16,7 @@ class ROIRecord { // Gestion flexible du format de timestamp (millisecondes ou ISO8601) DateTime parsedTimestamp; var timestampValue = json['timestamp']; - + if (timestampValue is int) { // Format milliseconds parsedTimestamp = DateTime.fromMillisecondsSinceEpoch(timestampValue); @@ -31,7 +33,7 @@ class ROIRecord { debugPrint("❌ Format de timestamp ROI non reconnu: $timestampValue"); parsedTimestamp = DateTime.now(); } - + return ROIRecord( timestamp: parsedTimestamp, roi: json['roi']?.toDouble() ?? 0.0, diff --git a/lib/models/wallet_balance_record.dart b/lib/models/wallet_balance_record.dart index ef10587..443ca73 100644 --- a/lib/models/wallet_balance_record.dart +++ b/lib/models/wallet_balance_record.dart @@ -20,4 +20,4 @@ class BalanceRecord { timestamp: DateTime.parse(json['timestamp'] as String), ); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/portfolio/charts/performance_by_region_chart.dart b/lib/pages/Statistics/portfolio/charts/performance_by_region_chart.dart index 52708b5..efe8229 100644 --- a/lib/pages/Statistics/portfolio/charts/performance_by_region_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/performance_by_region_chart.dart @@ -12,7 +12,8 @@ class PerformanceByRegionChart extends StatefulWidget { const PerformanceByRegionChart({super.key, required this.dataManager}); @override - _PerformanceByRegionChartState createState() => _PerformanceByRegionChartState(); + _PerformanceByRegionChartState createState() => + _PerformanceByRegionChartState(); } class _PerformanceByRegionChartState extends State { @@ -34,7 +35,7 @@ class _PerformanceByRegionChartState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -44,7 +45,7 @@ class _PerformanceByRegionChartState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -66,8 +67,10 @@ class _PerformanceByRegionChartState extends State { DropdownButton( value: _sortBy, items: [ - DropdownMenuItem(value: 'roi', child: Text(S.of(context).sortByROI)), - DropdownMenuItem(value: 'count', child: Text(S.of(context).sortByCount)), + DropdownMenuItem( + value: 'roi', child: Text(S.of(context).sortByROI)), + DropdownMenuItem( + value: 'count', child: Text(S.of(context).sortByCount)), ], onChanged: (String? value) { setState(() { @@ -106,9 +109,11 @@ class _PerformanceByRegionChartState extends State { for (var token in widget.dataManager.portfolio) { final String regionCode = token['regionCode'] ?? 'Unknown'; final String regionName = Parameters.getRegionDisplayName(regionCode); - final double totalRentReceived = (token['totalRentReceived'] ?? 0.0).toDouble(); + final double totalRentReceived = + (token['totalRentReceived'] ?? 0.0).toDouble(); final double initialValue = (token['initialValue'] ?? 0.0).toDouble(); - final double roi = initialValue > 0 ? (totalRentReceived / initialValue) * 100 : 0.0; + final double roi = + initialValue > 0 ? (totalRentReceived / initialValue) * 100 : 0.0; if (!regionData.containsKey(regionName)) { regionData[regionName] = { @@ -119,10 +124,14 @@ class _PerformanceByRegionChartState extends State { }; } - regionData[regionName]!['totalRoi'] = regionData[regionName]!['totalRoi'] + roi; - regionData[regionName]!['totalInitialValue'] = regionData[regionName]!['totalInitialValue'] + initialValue; - regionData[regionName]!['totalRentReceived'] = regionData[regionName]!['totalRentReceived'] + totalRentReceived; - regionData[regionName]!['tokenCount'] = regionData[regionName]!['tokenCount'] + 1; + regionData[regionName]!['totalRoi'] = + regionData[regionName]!['totalRoi'] + roi; + regionData[regionName]!['totalInitialValue'] = + regionData[regionName]!['totalInitialValue'] + initialValue; + regionData[regionName]!['totalRentReceived'] = + regionData[regionName]!['totalRentReceived'] + totalRentReceived; + regionData[regionName]!['tokenCount'] = + regionData[regionName]!['tokenCount'] + 1; } // Calculer le ROI moyen par région @@ -130,16 +139,19 @@ class _PerformanceByRegionChartState extends State { final int tokenCount = data['tokenCount']; final double totalInitialValue = data['totalInitialValue']; final double totalRentReceived = data['totalRentReceived']; - - data['averageRoi'] = totalInitialValue > 0 ? (totalRentReceived / totalInitialValue) * 100 : 0.0; + + data['averageRoi'] = totalInitialValue > 0 + ? (totalRentReceived / totalInitialValue) * 100 + : 0.0; }); return regionData; } Widget _buildRegionPerformanceChart(int? selectedIndex) { - final Map> regionData = _calculateRegionPerformance(); - + final Map> regionData = + _calculateRegionPerformance(); + if (regionData.isEmpty) { return Center( child: Text( @@ -153,21 +165,29 @@ class _PerformanceByRegionChartState extends State { } // Trier les données selon le critère sélectionné - final List>> sortedEntries = regionData.entries.toList(); + final List>> sortedEntries = + regionData.entries.toList(); if (_sortBy == 'roi') { - sortedEntries.sort((a, b) => b.value['averageRoi'].compareTo(a.value['averageRoi'])); + sortedEntries.sort( + (a, b) => b.value['averageRoi'].compareTo(a.value['averageRoi'])); } else { - sortedEntries.sort((a, b) => b.value['tokenCount'].compareTo(a.value['tokenCount'])); + sortedEntries.sort( + (a, b) => b.value['tokenCount'].compareTo(a.value['tokenCount'])); } // Limiter à 8 régions pour la lisibilité - final List>> displayEntries = + final List>> displayEntries = sortedEntries.take(8).toList(); return BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, - maxY: displayEntries.isEmpty ? 10 : displayEntries.map((e) => e.value['averageRoi'] as double).reduce((a, b) => a > b ? a : b) * 1.2, + maxY: displayEntries.isEmpty + ? 10 + : displayEntries + .map((e) => e.value['averageRoi'] as double) + .reduce((a, b) => a > b ? a : b) * + 1.2, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { @@ -175,11 +195,12 @@ class _PerformanceByRegionChartState extends State { final regionName = entry.key; final data = entry.value; return BarTooltipItem( - '$regionName\n${S.of(context).averageROI}: ${data['averageRoi'].toStringAsFixed(1)}%\nTokens: ${data['tokenCount']}', + '$regionName\n${S.of(context).averageROI}: ${data['averageRoi'].toStringAsFixed(1)}%\nTokens: ${data['tokenCount']}', TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 12 + Provider.of(context).getTextSizeOffset(), + fontSize: + 12 + Provider.of(context).getTextSizeOffset(), ), ); }, @@ -187,7 +208,8 @@ class _PerformanceByRegionChartState extends State { touchCallback: (FlTouchEvent event, BarTouchResponse? response) { if (response != null && response.spot != null) { final touchedIndex = response.spot!.touchedBarGroupIndex; - _selectedIndexNotifier.value = touchedIndex >= 0 ? touchedIndex : null; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } @@ -206,9 +228,12 @@ class _PerformanceByRegionChartState extends State { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - regionName.length > 6 ? '${regionName.substring(0, 6)}...' : regionName, + regionName.length > 6 + ? '${regionName.substring(0, 6)}...' + : regionName, style: TextStyle( - fontSize: 10 + Provider.of(context).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -226,15 +251,18 @@ class _PerformanceByRegionChartState extends State { return Text( '${value.toInt()}%', style: TextStyle( - fontSize: 10 + Provider.of(context).getTextSizeOffset(), + fontSize: + 10 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, ), ); }, ), ), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: + const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), barGroups: displayEntries.asMap().entries.map((entry) { @@ -260,7 +288,7 @@ class _PerformanceByRegionChartState extends State { barRods: [ BarChartRodData( toY: roi, - color: barColor.withOpacity(isSelected ? 1.0 : 0.8), + color: barColor.withValues(alpha: isSelected ? 1.0 : 0.8), width: 20, borderRadius: const BorderRadius.only( topLeft: Radius.circular(6), @@ -268,8 +296,11 @@ class _PerformanceByRegionChartState extends State { ), backDrawRodData: BackgroundBarChartRodData( show: true, - toY: displayEntries.map((e) => e.value['averageRoi'] as double).reduce((a, b) => a > b ? a : b) * 1.2, - color: Colors.grey.withOpacity(0.1), + toY: displayEntries + .map((e) => e.value['averageRoi'] as double) + .reduce((a, b) => a > b ? a : b) * + 1.2, + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -281,14 +312,18 @@ class _PerformanceByRegionChartState extends State { } Widget _buildRegionLegend() { - final Map> regionData = _calculateRegionPerformance(); + final Map> regionData = + _calculateRegionPerformance(); final appState = Provider.of(context); - final List>> sortedEntries = regionData.entries.toList(); + final List>> sortedEntries = + regionData.entries.toList(); if (_sortBy == 'roi') { - sortedEntries.sort((a, b) => b.value['averageRoi'].compareTo(a.value['averageRoi'])); + sortedEntries.sort( + (a, b) => b.value['averageRoi'].compareTo(a.value['averageRoi'])); } else { - sortedEntries.sort((a, b) => b.value['tokenCount'].compareTo(a.value['tokenCount'])); + sortedEntries.sort( + (a, b) => b.value['tokenCount'].compareTo(a.value['tokenCount'])); } return Wrap( @@ -316,16 +351,21 @@ class _PerformanceByRegionChartState extends State { return InkWell( onTap: () { - _selectedIndexNotifier.value = (_selectedIndexNotifier.value == index) ? null : index; + _selectedIndexNotifier.value = + (_selectedIndexNotifier.value == index) ? null : index; }, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: _selectedIndexNotifier.value == index ? color.withOpacity(0.1) : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: _selectedIndexNotifier.value == index ? color : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color + : Colors.transparent, width: 1, ), ), @@ -340,7 +380,7 @@ class _PerformanceByRegionChartState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ), @@ -352,8 +392,12 @@ class _PerformanceByRegionChartState extends State { '$regionName: ${roi.toStringAsFixed(1)}% ($tokenCount)', style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: _selectedIndexNotifier.value == index ? color : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: _selectedIndexNotifier.value == index ? FontWeight.w600 : FontWeight.normal, + color: _selectedIndexNotifier.value == index + ? color + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: _selectedIndexNotifier.value == index + ? FontWeight.w600 + : FontWeight.normal, ), ), ], @@ -363,4 +407,4 @@ class _PerformanceByRegionChartState extends State { }).toList(), ); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/portfolio/charts/rent_distribution_by_product_type_chart.dart b/lib/pages/Statistics/portfolio/charts/rent_distribution_by_product_type_chart.dart index 0b24851..4287f08 100644 --- a/lib/pages/Statistics/portfolio/charts/rent_distribution_by_product_type_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/rent_distribution_by_product_type_chart.dart @@ -14,10 +14,12 @@ class RentDistributionByProductTypeChart extends StatefulWidget { }); @override - State createState() => _RentDistributionByProductTypeChartState(); + State createState() => + _RentDistributionByProductTypeChartState(); } -class _RentDistributionByProductTypeChartState extends State { +class _RentDistributionByProductTypeChartState + extends State { final ValueNotifier _selectedIndexNotifier = ValueNotifier(null); @override @@ -35,7 +37,7 @@ class _RentDistributionByProductTypeChartState extends State _calculateRentByProductType() { Map rentByProductType = {}; - + for (var token in widget.dataManager.portfolio) { final String productType = token['productType'] ?? 'other'; - final double totalRentReceived = (token['totalRentReceived'] ?? 0.0).toDouble(); - + final double totalRentReceived = + (token['totalRentReceived'] ?? 0.0).toDouble(); + if (totalRentReceived > 0) { - rentByProductType[productType] = (rentByProductType[productType] ?? 0.0) + totalRentReceived; + rentByProductType[productType] = + (rentByProductType[productType] ?? 0.0) + totalRentReceived; } } - + return rentByProductType; } - String _getLocalizedProductTypeName(BuildContext context, String productType) { + String _getLocalizedProductTypeName( + BuildContext context, String productType) { switch (productType.toLowerCase()) { case 'real_estate_rental': return S.of(context).productTypeRealEstateRental; @@ -128,7 +133,7 @@ class _RentDistributionByProductTypeChartState extends State rentByProductType = _calculateRentByProductType(); - + if (rentByProductType.isEmpty) { return Center( child: Text( @@ -142,9 +147,11 @@ class _RentDistributionByProductTypeChartState extends State colors = _getColors(); - final double total = rentByProductType.values.fold(0.0, (sum, value) => sum + value); - - List> sortedData = rentByProductType.entries.toList() + final double total = + rentByProductType.values.fold(0.0, (sum, value) => sum + value); + + List> sortedData = rentByProductType.entries + .toList() ..sort((a, b) => b.value.compareTo(a.value)); return PieChart( @@ -154,7 +161,7 @@ class _RentDistributionByProductTypeChartState extends State= 0 ? touchedIndex : null; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } @@ -187,86 +195,93 @@ class _RentDistributionByProductTypeChartState extends State rentByProductType = _calculateRentByProductType(); final appState = Provider.of(context); - + if (rentByProductType.isEmpty) { return const SizedBox.shrink(); } final List colors = _getColors(); - final double total = rentByProductType.values.fold(0.0, (sum, value) => sum + value); - - List> sortedData = rentByProductType.entries.toList() + final double total = + rentByProductType.values.fold(0.0, (sum, value) => sum + value); + + List> sortedData = rentByProductType.entries + .toList() ..sort((a, b) => b.value.compareTo(a.value)); return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...sortedData.asMap().entries.map((entry) { - final int index = entry.key; - final String productType = entry.value.key; - final double percentage = (entry.value.value / total) * 100; - - return ValueListenableBuilder( - valueListenable: _selectedIndexNotifier, - builder: (context, selectedIndex, child) { - final bool isSelected = selectedIndex == index; - - return GestureDetector( - onTap: () { - _selectedIndexNotifier.value = isSelected ? null : index; - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 1), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: colors[index % colors.length], - borderRadius: BorderRadius.circular(4), - border: isSelected - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - _getLocalizedProductTypeName(context, productType), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - overflow: TextOverflow.ellipsis, - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...sortedData.asMap().entries.map((entry) { + final int index = entry.key; + final String productType = entry.value.key; + final double percentage = (entry.value.value / total) * 100; + + return ValueListenableBuilder( + valueListenable: _selectedIndexNotifier, + builder: (context, selectedIndex, child) { + final bool isSelected = selectedIndex == index; + + return GestureDetector( + onTap: () { + _selectedIndexNotifier.value = isSelected ? null : index; + }, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 1), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).primaryColor.withValues(alpha: 0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: colors[index % colors.length], + borderRadius: BorderRadius.circular(4), + border: isSelected + ? Border.all( + color: Theme.of(context).primaryColor, + width: 2) + : null, ), - Text( - '${percentage.toStringAsFixed(1)}%', + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _getLocalizedProductTypeName(context, productType), style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: isSelected - ? Theme.of(context).primaryColor - : Theme.of(context).textTheme.bodyLarge?.color, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: Theme.of(context).textTheme.bodyLarge?.color, ), + overflow: TextOverflow.ellipsis, ), - ], - ), + ), + Text( + '${percentage.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 13 + appState.getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: isSelected + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ], ), - ); - }, - ); - }).toList(), - ], - ); + ), + ); + }, + ); + }), + ], + ); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/portfolio/charts/rent_distribution_chart.dart b/lib/pages/Statistics/portfolio/charts/rent_distribution_chart.dart index 7a252f8..5e74cbb 100644 --- a/lib/pages/Statistics/portfolio/charts/rent_distribution_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/rent_distribution_chart.dart @@ -43,7 +43,7 @@ class _RentDistributionChartState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -53,7 +53,7 @@ class _RentDistributionChartState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -75,9 +75,12 @@ class _RentDistributionChartState extends State { DropdownButton( value: _selectedFilter, items: [ - DropdownMenuItem(value: 'Country', child: Text(S.of(context).country)), - DropdownMenuItem(value: 'Region', child: Text(S.of(context).region)), - DropdownMenuItem(value: 'City', child: Text(S.of(context).city)), + DropdownMenuItem( + value: 'Country', child: Text(S.of(context).country)), + DropdownMenuItem( + value: 'Region', child: Text(S.of(context).region)), + DropdownMenuItem( + value: 'City', child: Text(S.of(context).city)), ], onChanged: (String? value) { setState(() { @@ -104,17 +107,22 @@ class _RentDistributionChartState extends State { sectionsSpace: 3, borderData: FlBorderData(show: false), pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, PieTouchResponse? response) { - if (response != null && response.touchedSection != null) { - final touchedIndex = response.touchedSection!.touchedSectionIndex; - _selectedIndexNotifier.value = touchedIndex >= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterText(selectedIndex), @@ -152,7 +160,8 @@ class _RentDistributionChartState extends State { key = S.of(context).unknown; } - groupedData[key] = (groupedData[key] ?? 0) + (token['monthlyIncome'] ?? 0.0); + groupedData[key] = + (groupedData[key] ?? 0) + (token['monthlyIncome'] ?? 0.0); } return groupedData; @@ -167,13 +176,14 @@ class _RentDistributionChartState extends State { PieChartSectionData( value: 1, title: '', - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), radius: 40, ) ]; } - final sortedEntries = groupedData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = groupedData.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); return sortedEntries.asMap().entries.map((entry) { final index = entry.key; @@ -186,15 +196,17 @@ class _RentDistributionChartState extends State { return PieChartSectionData( value: data.value, title: percentage < 5 ? '' : '${percentage.toStringAsFixed(1)}%', - color: generateColor(index).withOpacity(opacity), + color: generateColor(index).withValues(alpha: opacity), radius: isSelected ? 52 : 45, titleStyle: TextStyle( - fontSize: isSelected ? 14 + Provider.of(context).getTextSizeOffset() : 10 + Provider.of(context).getTextSizeOffset(), + fontSize: isSelected + ? 14 + Provider.of(context).getTextSizeOffset() + : 10 + Provider.of(context).getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -219,7 +231,7 @@ class _RentDistributionChartState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: const Offset(0, 1), ), @@ -247,7 +259,8 @@ class _RentDistributionChartState extends State { ), const SizedBox(height: 4), Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(totalRent), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount(currencyUtils.convert(totalRent), + currencyUtils.currencySymbol, true), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -259,7 +272,8 @@ class _RentDistributionChartState extends State { } // Obtenir l'entrée sélectionnée - final sortedEntries = groupedData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = groupedData.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); if (selectedIndex >= sortedEntries.length) return Container(); @@ -279,7 +293,10 @@ class _RentDistributionChartState extends State { ), const SizedBox(height: 4), Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(selectedEntry.value), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(selectedEntry.value), + currencyUtils.currencySymbol, + true), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -295,7 +312,8 @@ class _RentDistributionChartState extends State { final appState = Provider.of(context); final currencyUtils = Provider.of(context, listen: false); - final sortedEntries = groupedData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = groupedData.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); return Wrap( spacing: 8.0, @@ -308,16 +326,21 @@ class _RentDistributionChartState extends State { return InkWell( onTap: () { - _selectedIndexNotifier.value = (_selectedIndexNotifier.value == index) ? null : index; + _selectedIndexNotifier.value = + (_selectedIndexNotifier.value == index) ? null : index; }, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: _selectedIndexNotifier.value == index ? color.withOpacity(0.1) : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: _selectedIndexNotifier.value == index ? color : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color + : Colors.transparent, width: 1, ), ), @@ -332,7 +355,7 @@ class _RentDistributionChartState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ), @@ -341,11 +364,15 @@ class _RentDistributionChartState extends State { ), const SizedBox(width: 8), Text( - '${data.key}', + data.key, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: _selectedIndexNotifier.value == index ? color : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: _selectedIndexNotifier.value == index ? FontWeight.w600 : FontWeight.normal, + color: _selectedIndexNotifier.value == index + ? color + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: _selectedIndexNotifier.value == index + ? FontWeight.w600 + : FontWeight.normal, ), ), ], diff --git a/lib/pages/Statistics/portfolio/charts/rental_status_distribution_chart.dart b/lib/pages/Statistics/portfolio/charts/rental_status_distribution_chart.dart index 1e871df..05f7ca0 100644 --- a/lib/pages/Statistics/portfolio/charts/rental_status_distribution_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/rental_status_distribution_chart.dart @@ -11,10 +11,12 @@ class RentalStatusDistributionChart extends StatefulWidget { const RentalStatusDistributionChart({super.key, required this.dataManager}); @override - _RentalStatusDistributionChartState createState() => _RentalStatusDistributionChartState(); + _RentalStatusDistributionChartState createState() => + _RentalStatusDistributionChartState(); } -class _RentalStatusDistributionChartState extends State { +class _RentalStatusDistributionChartState + extends State { final ValueNotifier _selectedIndexNotifier = ValueNotifier(null); @override @@ -32,7 +34,7 @@ class _RentalStatusDistributionChartState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterText(selectedIndex), @@ -137,14 +144,14 @@ class _RentalStatusDistributionChartState extends State> sortedEntries = statusData.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + statusData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); return sortedEntries.asMap().entries.map((entry) { final index = entry.key; @@ -173,15 +180,17 @@ class _RentalStatusDistributionChartState extends State(context).getTextSizeOffset() : 10 + Provider.of(context).getTextSizeOffset(), + fontSize: isSelected + ? 14 + Provider.of(context).getTextSizeOffset() + : 10 + Provider.of(context).getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -206,7 +215,7 @@ class _RentalStatusDistributionChartState extends State> sortedEntries = statusData.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + statusData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); if (selectedIndex >= sortedEntries.length) return Container(); @@ -307,8 +316,8 @@ class _RentalStatusDistributionChartState extends State statusData = _calculateRentalStatusData(); final appState = Provider.of(context); - final List> sortedEntries = statusData.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + statusData.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); return Wrap( spacing: 8.0, @@ -324,16 +333,21 @@ class _RentalStatusDistributionChartState extends State(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -53,9 +55,12 @@ class RentedHistoryGraph extends StatelessWidget { } // Récupérer les données pour les graphiques - List rentedHistoryData = _buildRentedHistoryChartData(context, dataManager, selectedPeriod); - List barChartData = _buildRentedHistoryBarChartData(context, dataManager, selectedPeriod); - List dateLabels = _buildDateLabelsForRented(context, dataManager, selectedPeriod); + List rentedHistoryData = + _buildRentedHistoryChartData(context, dataManager, selectedPeriod); + List barChartData = + _buildRentedHistoryBarChartData(context, dataManager, selectedPeriod); + List dateLabels = + _buildDateLabelsForRented(context, dataManager, selectedPeriod); return Card( elevation: 0, @@ -68,7 +73,7 @@ class RentedHistoryGraph extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -78,7 +83,7 @@ class RentedHistoryGraph extends StatelessWidget { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -104,7 +109,8 @@ class RentedHistoryGraph extends StatelessWidget { color: Theme.of(context).primaryColor, ), style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + backgroundColor: + Theme.of(context).primaryColor.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -131,7 +137,8 @@ class RentedHistoryGraph extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only(bottom: 12.0, left: 8.0), + padding: const EdgeInsets.only( + bottom: 12.0, left: 8.0), child: Text( "Type de graphique", style: TextStyle( @@ -217,7 +224,7 @@ class RentedHistoryGraph extends StatelessWidget { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, @@ -229,7 +236,8 @@ class RentedHistoryGraph extends StatelessWidget { reservedSize: 45, getTitlesWidget: (value, meta) { return Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: + const EdgeInsets.only(right: 8.0), child: Text( '${value.toStringAsFixed(0)}%', style: TextStyle( @@ -245,9 +253,11 @@ class RentedHistoryGraph extends StatelessWidget { sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { - if (value.toInt() >= 0 && value.toInt() < dateLabels.length) { + if (value.toInt() >= 0 && + value.toInt() < dateLabels.length) { return Padding( - padding: const EdgeInsets.only(top: 10.0), + padding: + const EdgeInsets.only(top: 10.0), child: Transform.rotate( angle: -0.5, child: Text( @@ -282,11 +292,12 @@ class RentedHistoryGraph extends StatelessWidget { toY: group.barRods.first.toY, color: const Color(0xFF007AFF), width: 12, - borderRadius: const BorderRadius.all(Radius.circular(6)), + borderRadius: const BorderRadius.all( + Radius.circular(6)), backDrawRodData: BackgroundBarChartRodData( show: true, toY: 100, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -294,7 +305,8 @@ class RentedHistoryGraph extends StatelessWidget { }).toList(), barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( - getTooltipItem: (group, groupIndex, rod, rodIndex) { + getTooltipItem: + (group, groupIndex, rod, rodIndex) { final periodLabel = dateLabels[groupIndex]; return BarTooltipItem( '$periodLabel\n${rod.toY.toStringAsFixed(1)}%', @@ -320,26 +332,30 @@ class RentedHistoryGraph extends StatelessWidget { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false)), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 45, getTitlesWidget: (value, meta) { - final highestValue = rentedHistoryData.map((e) => e.y).reduce((a, b) => a > b ? a : b); + final highestValue = rentedHistoryData + .map((e) => e.y) + .reduce((a, b) => a > b ? a : b); if (value == highestValue) { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: + const EdgeInsets.only(right: 8.0), child: Text( '${value.toStringAsFixed(0)}%', style: TextStyle( @@ -355,11 +371,15 @@ class RentedHistoryGraph extends StatelessWidget { sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { - List labels = _buildDateLabelsForRented(context, dataManager, selectedPeriod); + List labels = + _buildDateLabelsForRented(context, + dataManager, selectedPeriod); - if (value.toInt() >= 0 && value.toInt() < labels.length) { + if (value.toInt() >= 0 && + value.toInt() < labels.length) { return Padding( - padding: const EdgeInsets.only(top: 10.0), + padding: + const EdgeInsets.only(top: 10.0), child: Transform.rotate( angle: -0.5, child: Text( @@ -392,7 +412,8 @@ class RentedHistoryGraph extends StatelessWidget { color: const Color(0xFF34C759), dotData: FlDotData( show: true, - getDotPainter: (spot, percent, barData, index) { + getDotPainter: + (spot, percent, barData, index) { return FlDotCirclePainter( radius: 3, color: Colors.white, @@ -403,8 +424,13 @@ class RentedHistoryGraph extends StatelessWidget { checkToShowDot: (spot, barData) { // Montrer les points uniquement aux extrémités et points intermédiaires final isFirst = spot.x == 0; - final isLast = spot.x == barData.spots.length - 1; - final isInteresting = spot.x % (barData.spots.length > 10 ? 5 : 2) == 0; + final isLast = + spot.x == barData.spots.length - 1; + final isInteresting = spot.x % + (barData.spots.length > 10 + ? 5 + : 2) == + 0; return isFirst || isLast || isInteresting; }, ), @@ -412,8 +438,10 @@ class RentedHistoryGraph extends StatelessWidget { show: true, gradient: LinearGradient( colors: [ - const Color(0xFF34C759).withOpacity(0.3), - const Color(0xFF34C759).withOpacity(0.05), + const Color(0xFF34C759) + .withValues(alpha: 0.3), + const Color(0xFF34C759) + .withValues(alpha: 0.05), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -423,11 +451,14 @@ class RentedHistoryGraph extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - getTooltipItems: (List touchedSpots) { + getTooltipItems: + (List touchedSpots) { return touchedSpots.map((touchedSpot) { final index = touchedSpot.x.toInt(); final value = touchedSpot.y; - final String periodLabel = _buildDateLabelsForRented(context, dataManager, selectedPeriod)[index]; + final String periodLabel = + _buildDateLabelsForRented(context, + dataManager, selectedPeriod)[index]; return LineTooltipItem( '$periodLabel\n${value.toStringAsFixed(1)}%', @@ -460,7 +491,8 @@ class RentedHistoryGraph extends StatelessWidget { ); } - List _buildRentedHistoryChartData(BuildContext context, DataManager dataManager, String selectedPeriod) { + List _buildRentedHistoryChartData( + BuildContext context, DataManager dataManager, String selectedPeriod) { return ChartUtils.buildHistoryChartData( context, dataManager.rentedHistory, @@ -470,8 +502,10 @@ class RentedHistoryGraph extends StatelessWidget { ); } - List _buildRentedHistoryBarChartData(BuildContext context, DataManager dataManager, String selectedPeriod) { - List rentedHistoryData = _buildRentedHistoryChartData(context, dataManager, selectedPeriod); + List _buildRentedHistoryBarChartData( + BuildContext context, DataManager dataManager, String selectedPeriod) { + List rentedHistoryData = + _buildRentedHistoryChartData(context, dataManager, selectedPeriod); return rentedHistoryData .asMap() .entries @@ -487,7 +521,7 @@ class RentedHistoryGraph extends StatelessWidget { backDrawRodData: BackgroundBarChartRodData( show: true, toY: 100, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -496,7 +530,8 @@ class RentedHistoryGraph extends StatelessWidget { .toList(); } - List _buildDateLabelsForRented(BuildContext context, DataManager dataManager, String selectedPeriod) { + List _buildDateLabelsForRented( + BuildContext context, DataManager dataManager, String selectedPeriod) { List rentedHistory = dataManager.rentedHistory; // Grouper les données en fonction de la période sélectionnée @@ -508,7 +543,8 @@ class RentedHistoryGraph extends StatelessWidget { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); // Grouper par jour } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -529,12 +565,10 @@ class RentedHistoryGraph extends StatelessWidget { final dataManager = Provider.of(context, listen: false); for (var token in dataManager.portfolio) { - - // Vérifier si le token a un revenu mensuel positif if (token['monthlyIncome'] != null) { double monthlyIncome = (token['monthlyIncome'] ?? 0.0).toDouble(); - + // Vérifier si la valeur est valide if (monthlyIncome > 0) { spots.add(FlSpot(index.toDouble(), monthlyIncome)); @@ -571,7 +605,7 @@ class RentedHistoryGraph extends StatelessWidget { backDrawRodData: BackgroundBarChartRodData( show: true, toY: _calculateMaxY(context), - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], diff --git a/lib/pages/Statistics/portfolio/charts/roi_by_token_chart.dart b/lib/pages/Statistics/portfolio/charts/roi_by_token_chart.dart index c3a955d..74a24ca 100644 --- a/lib/pages/Statistics/portfolio/charts/roi_by_token_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/roi_by_token_chart.dart @@ -34,7 +34,7 @@ class _RoiByTokenChartState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -44,7 +44,7 @@ class _RoiByTokenChartState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -57,7 +57,9 @@ class _RoiByTokenChartState extends State { Text( S.of(context).roiByToken, style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.5, ), @@ -70,7 +72,8 @@ class _RoiByTokenChartState extends State { color: Theme.of(context).primaryColor, ), style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + backgroundColor: + Theme.of(context).primaryColor.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -132,7 +135,9 @@ class _RoiByTokenChartState extends State { size: 28, ), title: Text( - _showTopOnly ? S.of(context).showTop10 : S.of(context).showAll, + _showTopOnly + ? S.of(context).showTop10 + : S.of(context).showAll, style: TextStyle( fontWeight: FontWeight.w500, fontSize: 16, @@ -167,7 +172,9 @@ class _RoiByTokenChartState extends State { value: _sortBy, items: [ DropdownMenuItem(value: 'roi', child: Text('ROI')), - DropdownMenuItem(value: 'rent', child: Text(S.of(context).totalRent)), + DropdownMenuItem( + value: 'rent', + child: Text(S.of(context).totalRent)), ], onChanged: (String? value) { if (value != null) { @@ -192,7 +199,7 @@ class _RoiByTokenChartState extends State { Widget _buildRoiChart(BuildContext context) { final List> roiData = _calculateRoiData(); - + if (roiData.isEmpty) { return Center( child: Text( @@ -216,7 +223,8 @@ class _RoiByTokenChartState extends State { touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { final token = roiData[groupIndex]; - final currencyUtils = Provider.of(context, listen: false); + final currencyUtils = + Provider.of(context, listen: false); return BarTooltipItem( '${token['shortName']}\nROI: ${token['roi'].toStringAsFixed(2)}%\n${S.of(context).totalRent}: ${currencyUtils.getFormattedAmount(currencyUtils.convert(token['totalRent']), currencyUtils.currencySymbol, true)}', TextStyle( @@ -234,7 +242,8 @@ class _RoiByTokenChartState extends State { ), titlesData: FlTitlesData( show: true, - rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( @@ -248,7 +257,9 @@ class _RoiByTokenChartState extends State { child: Text( roiData[value.toInt()]['shortName'], style: TextStyle( - fontSize: 10 + Provider.of(context).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context) + .getTextSizeOffset(), color: Colors.grey.shade600, ), overflow: TextOverflow.ellipsis, @@ -271,7 +282,9 @@ class _RoiByTokenChartState extends State { child: Text( '${value.toStringAsFixed(0)}%', style: TextStyle( - fontSize: 10 + Provider.of(context).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context) + .getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -285,7 +298,7 @@ class _RoiByTokenChartState extends State { final index = entry.key; final data = entry.value; final roi = data['roi']; - + Color barColor; if (roi >= 10) { barColor = const Color(0xFF34C759); // Vert pour ROI élevé @@ -314,7 +327,7 @@ class _RoiByTokenChartState extends State { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, @@ -329,13 +342,15 @@ class _RoiByTokenChartState extends State { List> roiData = []; for (var token in widget.dataManager.portfolio) { - final double totalRentReceived = (token['totalRentReceived'] ?? 0.0).toDouble(); - final double initialTotalValue = (token['initialTotalValue'] ?? 0.0).toDouble(); - + final double totalRentReceived = + (token['totalRentReceived'] ?? 0.0).toDouble(); + final double initialTotalValue = + (token['initialTotalValue'] ?? 0.0).toDouble(); + // Calculer le ROI seulement si on a un investissement initial if (initialTotalValue > 0.001) { final double roi = (totalRentReceived / initialTotalValue) * 100; - + roiData.add({ 'shortName': token['shortName'] ?? 'Unknown', 'fullName': token['fullName'] ?? 'Unknown', @@ -360,4 +375,4 @@ class _RoiByTokenChartState extends State { return roiData; } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/portfolio/charts/token_count_evolution_chart.dart b/lib/pages/Statistics/portfolio/charts/token_count_evolution_chart.dart index fb2f28c..09aa273 100644 --- a/lib/pages/Statistics/portfolio/charts/token_count_evolution_chart.dart +++ b/lib/pages/Statistics/portfolio/charts/token_count_evolution_chart.dart @@ -12,7 +12,8 @@ class TokenCountEvolutionChart extends StatefulWidget { const TokenCountEvolutionChart({super.key, required this.dataManager}); @override - _TokenCountEvolutionChartState createState() => _TokenCountEvolutionChartState(); + _TokenCountEvolutionChartState createState() => + _TokenCountEvolutionChartState(); } class _TokenCountEvolutionChartState extends State { @@ -34,7 +35,7 @@ class _TokenCountEvolutionChartState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -44,7 +45,7 @@ class _TokenCountEvolutionChartState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -57,7 +58,9 @@ class _TokenCountEvolutionChartState extends State { Text( S.of(context).tokenCountEvolution, style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.5, ), @@ -68,9 +71,12 @@ class _TokenCountEvolutionChartState extends State { DropdownButton( value: _selectedPeriod, items: [ - DropdownMenuItem(value: 'week', child: Text(S.of(context).week)), - DropdownMenuItem(value: 'month', child: Text(S.of(context).month)), - DropdownMenuItem(value: 'year', child: Text(S.of(context).year)), + DropdownMenuItem( + value: 'week', child: Text(S.of(context).week)), + DropdownMenuItem( + value: 'month', child: Text(S.of(context).month)), + DropdownMenuItem( + value: 'year', child: Text(S.of(context).year)), ], onChanged: (String? value) { setState(() { @@ -81,12 +87,16 @@ class _TokenCountEvolutionChartState extends State { const SizedBox(width: 8), IconButton( icon: Icon( - _isBarChart ? Icons.show_chart_rounded : Icons.bar_chart_rounded, + _isBarChart + ? Icons.show_chart_rounded + : Icons.bar_chart_rounded, size: 20.0, color: Theme.of(context).primaryColor, ), style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + backgroundColor: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -131,7 +141,8 @@ class _TokenCountEvolutionChartState extends State { return BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, - maxY: tokenCountData.map((e) => e.y).reduce((a, b) => a > b ? a : b) * 1.1, + maxY: tokenCountData.map((e) => e.y).reduce((a, b) => a > b ? a : b) * + 1.1, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { @@ -225,7 +236,7 @@ class _TokenCountEvolutionChartState extends State { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, @@ -241,7 +252,7 @@ class _TokenCountEvolutionChartState extends State { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, ); }, @@ -313,8 +324,8 @@ class _TokenCountEvolutionChartState extends State { show: true, gradient: LinearGradient( colors: [ - const Color(0xFF34C759).withOpacity(0.3), - const Color(0xFF34C759).withOpacity(0.05), + const Color(0xFF34C759).withValues(alpha: 0.3), + const Color(0xFF34C759).withValues(alpha: 0.05), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -329,7 +340,8 @@ class _TokenCountEvolutionChartState extends State { final index = touchedSpot.x.toInt(); final value = touchedSpot.y; final labels = _buildDateLabels(); - final String periodLabel = index < labels.length ? labels[index] : ''; + final String periodLabel = + index < labels.length ? labels[index] : ''; return LineTooltipItem( '$periodLabel\n${value.toStringAsFixed(0)} tokens', @@ -358,8 +370,9 @@ class _TokenCountEvolutionChartState extends State { List _calculateTokenCountEvolution() { // Collecter toutes les transactions avec leurs dates List> allTransactions = []; - - for (var tokenTransactions in widget.dataManager.transactionsByToken.values) { + + for (var tokenTransactions + in widget.dataManager.transactionsByToken.values) { for (var transaction in tokenTransactions) { final timestamp = transaction['timestamp']; if (timestamp != null) { @@ -375,7 +388,7 @@ class _TokenCountEvolutionChartState extends State { } else { continue; } - + allTransactions.add({ 'date': date, 'type': transaction['transactionType'] ?? 'unknown', @@ -400,7 +413,7 @@ class _TokenCountEvolutionChartState extends State { final DateTime date = transaction['date']; final String tokenId = transaction['tokenId']; final String type = transaction['type']; - + String periodKey; if (_selectedPeriod == 'week') { final weekStart = date.subtract(Duration(days: date.weekday - 1)); @@ -434,8 +447,9 @@ class _TokenCountEvolutionChartState extends State { List _buildDateLabels() { List> allTransactions = []; - - for (var tokenTransactions in widget.dataManager.transactionsByToken.values) { + + for (var tokenTransactions + in widget.dataManager.transactionsByToken.values) { for (var transaction in tokenTransactions) { final timestamp = transaction['timestamp']; if (timestamp != null) { @@ -451,7 +465,7 @@ class _TokenCountEvolutionChartState extends State { } else { continue; } - + allTransactions.add({ 'date': date, 'type': transaction['transactionType'] ?? 'unknown', @@ -474,7 +488,7 @@ class _TokenCountEvolutionChartState extends State { final DateTime date = transaction['date']; final String tokenId = transaction['tokenId']; final String type = transaction['type']; - + String periodKey; if (_selectedPeriod == 'week') { final weekStart = date.subtract(Duration(days: date.weekday - 1)); @@ -494,4 +508,4 @@ class _TokenCountEvolutionChartState extends State { return tokensByPeriod.keys.toList()..sort(); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/portfolio/charts/token_distribution_by_city_card.dart b/lib/pages/Statistics/portfolio/charts/token_distribution_by_city_card.dart index 61d2a1e..44ce3ce 100644 --- a/lib/pages/Statistics/portfolio/charts/token_distribution_by_city_card.dart +++ b/lib/pages/Statistics/portfolio/charts/token_distribution_by_city_card.dart @@ -13,13 +13,17 @@ class TokenDistributionByCityCard extends StatefulWidget { const TokenDistributionByCityCard({super.key, required this.dataManager}); @override - _TokenDistributionByCityCardState createState() => _TokenDistributionByCityCardState(); + _TokenDistributionByCityCardState createState() => + _TokenDistributionByCityCardState(); } -class _TokenDistributionByCityCardState extends State { +class _TokenDistributionByCityCardState + extends State { int? _selectedIndexCity; - final ValueNotifier _selectedIndexNotifierCity = ValueNotifier(null); - List> othersDetails = []; // Pour stocker les détails de la section "Autres" + final ValueNotifier _selectedIndexNotifierCity = + ValueNotifier(null); + List> othersDetails = + []; // Pour stocker les détails de la section "Autres" @override Widget build(BuildContext context) { @@ -36,7 +40,7 @@ class _TokenDistributionByCityCardState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifierCity.value = + touchedIndex >= 0 ? touchedIndex : null; if (event is FlTapUpEvent) { - final section = response.touchedSection!.touchedSection; - if (section!.title.contains(S.of(context).others)) { - showOtherDetailsModal(context, widget.dataManager, othersDetails, 'city'); + final section = + response.touchedSection!.touchedSection; + if (section!.title + .contains(S.of(context).others)) { + showOtherDetailsModal( + context, + widget.dataManager, + othersDetails, + 'city'); } } } else { @@ -95,10 +110,12 @@ class _TokenDistributionByCityCardState extends State _buildDonutChartDataByCity(DataManager dataManager, List> othersDetails, int? selectedIndex) { + List _buildDonutChartDataByCity(DataManager dataManager, + List> othersDetails, int? selectedIndex) { Map cityCount = {}; final appState = Provider.of(context); @@ -130,7 +148,8 @@ class _TokenDistributionByCityCardState extends State sum + value); // Trier les villes par 'count' croissant - final sortedCities = cityCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedCities = cityCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List sections = []; othersDetails.clear(); // Clear previous details of "Autres" @@ -141,7 +160,8 @@ class _TokenDistributionByCityCardState extends State> othersDetails) { + Widget _buildCenterTextByCity(DataManager dataManager, int? selectedIndex, + List> othersDetails) { Map cityCount = {}; // Remplir le dictionnaire avec les counts par ville @@ -263,7 +296,8 @@ class _TokenDistributionByCityCardState extends State b.value.compareTo(a.value)); + final sortedCities = cityCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); if (selectedIndex < sortedCities.length) { final selectedCity = sortedCities[selectedIndex]; @@ -304,7 +338,9 @@ class _TokenDistributionByCityCardState extends State(0, (sum, item) => sum + (item['count'] as int)).toString(), + othersDetails + .fold(0, (sum, item) => sum + (item['count'] as int)) + .toString(), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -316,7 +352,8 @@ class _TokenDistributionByCityCardState extends State> othersDetails) { + Widget _buildLegendByCity( + DataManager dataManager, List> othersDetails) { Map cityCount = {}; final appState = Provider.of(context); @@ -330,7 +367,8 @@ class _TokenDistributionByCityCardState extends State sum + value); // Trier les villes par 'count' croissant - final sortedCities = cityCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedCities = cityCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List legendItems = []; int othersValue = 0; @@ -346,23 +384,30 @@ class _TokenDistributionByCityCardState extends State _TokenDistributionByCountryCardState(); + _TokenDistributionByCountryCardState createState() => + _TokenDistributionByCountryCardState(); } -class _TokenDistributionByCountryCardState extends State { +class _TokenDistributionByCountryCardState + extends State { int? _selectedIndexCountry; - final ValueNotifier _selectedIndexNotifierCountry = ValueNotifier(null); + final ValueNotifier _selectedIndexNotifierCountry = + ValueNotifier(null); @override Widget build(BuildContext context) { @@ -35,7 +36,7 @@ class _TokenDistributionByCountryCardState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifierCountry.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifierCountry.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), - _buildCenterTextByCountry(widget.dataManager, selectedIndex), + _buildCenterTextByCountry( + widget.dataManager, selectedIndex), ], ); }, @@ -108,11 +116,13 @@ class _TokenDistributionByCountryCardState extends State _buildDonutChartDataByCountry(DataManager dataManager, int? selectedIndex) { + List _buildDonutChartDataByCountry( + DataManager dataManager, int? selectedIndex) { Map countryCount = {}; final appState = Provider.of(context); - print('Nombre de tokens dans le portfolio: ${dataManager.portfolio.length}'); + print( + 'Nombre de tokens dans le portfolio: ${dataManager.portfolio.length}'); // Remplir le dictionnaire avec les counts par pays for (var token in dataManager.portfolio) { @@ -134,7 +144,8 @@ class _TokenDistributionByCountryCardState extends State a + b)) * 100; + final double percentage = + (value / countryCount.values.reduce((a, b) => a + b)) * 100; final bool isSelected = selectedIndex == index; final opacity = selectedIndex != null && !isSelected ? 0.5 : 1.0; @@ -145,15 +156,17 @@ class _TokenDistributionByCountryCardState extends State countryCount = {}; // Remplir le dictionnaire avec les counts par pays @@ -276,16 +290,21 @@ class _TokenDistributionByCountryCardState extends State _TokenDistributionByRegionCardState(); + _TokenDistributionByRegionCardState createState() => + _TokenDistributionByRegionCardState(); } -class _TokenDistributionByRegionCardState extends State { +class _TokenDistributionByRegionCardState + extends State { int? _selectedIndexRegion; - final ValueNotifier _selectedIndexNotifierRegion = ValueNotifier(null); - List> othersDetails = []; // Pour stocker les détails de la section "Autres" + final ValueNotifier _selectedIndexNotifierRegion = + ValueNotifier(null); + List> othersDetails = + []; // Pour stocker les détails de la section "Autres" @override Widget build(BuildContext context) { @@ -38,7 +41,7 @@ class _TokenDistributionByRegionCardState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifierRegion.value = + touchedIndex >= 0 ? touchedIndex : null; if (event is FlTapUpEvent) { - final section = response.touchedSection!.touchedSection; - if (section!.title.contains(S.of(context).others)) { - showOtherDetailsModal(context, widget.dataManager, othersDetails, 'region'); + final section = + response.touchedSection!.touchedSection; + if (section!.title + .contains(S.of(context).others)) { + showOtherDetailsModal( + context, + widget.dataManager, + othersDetails, + 'region'); } } } else { @@ -97,10 +111,12 @@ class _TokenDistributionByRegionCardState extends State _buildDonutChartDataByRegion(DataManager dataManager, List> othersDetails, int? selectedIndex) { + List _buildDonutChartDataByRegion( + DataManager dataManager, + List> othersDetails, + int? selectedIndex) { Map regionCount = {}; final appState = Provider.of(context); // Remplir le dictionnaire avec les counts par région (utilise LocationUtils) for (var token in dataManager.portfolio) { - String regionCode = token['regionCode'] ?? LocationUtils.extractRegion(token['fullName'] ?? ''); + String regionCode = token['regionCode'] ?? + LocationUtils.extractRegion(token['fullName'] ?? ''); String regionName = Parameters.getRegionDisplayName(regionCode); regionCount[regionName] = (regionCount[regionName] ?? 0) + 1; } @@ -133,7 +153,8 @@ class _TokenDistributionByRegionCardState extends State sum + value); // Trier les régions par 'count' croissant - final sortedRegions = regionCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedRegions = regionCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List sections = []; othersDetails.clear(); @@ -157,15 +178,17 @@ class _TokenDistributionByRegionCardState extends State> othersDetails) { + Widget _buildCenterTextByRegion(DataManager dataManager, int? selectedIndex, + List> othersDetails) { Map regionCount = {}; // Remplir le dictionnaire avec les counts par région (utilise LocationUtils) for (var token in dataManager.portfolio) { - String regionCode = token['regionCode'] ?? LocationUtils.extractRegion(token['fullName'] ?? ''); + String regionCode = token['regionCode'] ?? + LocationUtils.extractRegion(token['fullName'] ?? ''); String regionName = Parameters.getRegionDisplayName(regionCode); regionCount[regionName] = (regionCount[regionName] ?? 0) + 1; } @@ -269,7 +299,8 @@ class _TokenDistributionByRegionCardState extends State b.value.compareTo(a.value)); + final sortedRegions = regionCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); // Filtrer les régions dont le pourcentage est < 2% List> visibleRegions = []; @@ -324,7 +355,9 @@ class _TokenDistributionByRegionCardState extends State(0, (sum, item) => sum + (item['count'] as int)).toString(), + othersDetails + .fold(0, (sum, item) => sum + (item['count'] as int)) + .toString(), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -336,7 +369,8 @@ class _TokenDistributionByRegionCardState extends State> othersDetails) { + Widget _buildLegendByRegion( + DataManager dataManager, List> othersDetails) { Map regionCount = {}; final appState = Provider.of(context); @@ -351,7 +385,8 @@ class _TokenDistributionByRegionCardState extends State sum + value); // Trier les régions par 'count' croissant - final sortedRegions = regionCount.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedRegions = regionCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); List legendItems = []; int othersValue = 0; @@ -372,16 +407,24 @@ class _TokenDistributionByRegionCardState extends State _TokenDistributionByWalletCardState(); + _TokenDistributionByWalletCardState createState() => + _TokenDistributionByWalletCardState(); } -class _TokenDistributionByWalletCardState extends State { +class _TokenDistributionByWalletCardState + extends State { int? _selectedIndexWallet; - final ValueNotifier _selectedIndexNotifierWallet = ValueNotifier(null); + final ValueNotifier _selectedIndexNotifierWallet = + ValueNotifier(null); @override Widget build(BuildContext context) { @@ -35,7 +37,7 @@ class _TokenDistributionByWalletCardState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifierWallet.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifierWallet.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), - _buildCenterTextByWallet(widget.dataManager, selectedIndex), + _buildCenterTextByWallet( + widget.dataManager, selectedIndex), ], ); }, @@ -108,7 +117,8 @@ class _TokenDistributionByWalletCardState extends State _buildDonutChartDataByWallet(DataManager dataManager, int? selectedIndex) { + List _buildDonutChartDataByWallet( + DataManager dataManager, int? selectedIndex) { final appState = Provider.of(context); // Créer une Map qui compte les tokens par wallet @@ -120,14 +130,17 @@ class _TokenDistributionByWalletCardState extends State sum + count); + int totalWalletTokens = + walletTokenCounts.values.fold(0, (sum, count) => sum + count); // Trier les wallets par nombre de tokens (décroissant) - final sortedWallets = walletTokenCounts.keys.toList()..sort((a, b) => walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); + final sortedWallets = walletTokenCounts.keys.toList() + ..sort((a, b) => walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); // Créer les sections du graphique à secteurs return sortedWallets.asMap().entries.map((entry) { @@ -145,15 +158,17 @@ class _TokenDistributionByWalletCardState extends State sum + count); + int totalWalletTokens = + walletTokenCounts.values.fold(0, (sum, count) => sum + count); if (selectedIndex == null) { // Afficher la valeur totale si aucun segment n'est sélectionné @@ -228,7 +245,8 @@ class _TokenDistributionByWalletCardState extends State walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); + final sortedWallets = walletTokenCounts.keys.toList() + ..sort((a, b) => walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); if (selectedIndex >= sortedWallets.length) return Container(); @@ -270,14 +288,17 @@ class _TokenDistributionByWalletCardState extends State sum + count); + int totalWalletTokens = + walletTokenCounts.values.fold(0, (sum, count) => sum + count); // Trier les wallets par nombre de tokens (décroissant) - final sortedWallets = walletTokenCounts.keys.toList()..sort((a, b) => walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); + final sortedWallets = walletTokenCounts.keys.toList() + ..sort((a, b) => walletTokenCounts[b]!.compareTo(walletTokenCounts[a]!)); return Wrap( spacing: 12.0, @@ -292,16 +313,21 @@ class _TokenDistributionByWalletCardState extends State { int? _selectedIndexToken; - final ValueNotifier _selectedIndexNotifierToken = ValueNotifier(null); + final ValueNotifier _selectedIndexNotifierToken = + ValueNotifier(null); @override Widget build(BuildContext context) { @@ -59,7 +58,7 @@ class _TokenDistributionCardState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -69,7 +68,7 @@ class _TokenDistributionCardState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -101,17 +100,22 @@ class _TokenDistributionCardState extends State { sectionsSpace: 3, borderData: FlBorderData(show: false), pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, PieTouchResponse? response) { - if (response != null && response.touchedSection != null) { - final touchedIndex = response.touchedSection!.touchedSectionIndex; - _selectedIndexNotifierToken.value = touchedIndex >= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifierToken.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifierToken.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterTextToken(selectedIndex), @@ -141,23 +145,27 @@ class _TokenDistributionCardState extends State { PieChartSectionData( value: 1, title: '', - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), radius: 45, ) ]; } - print('Nombre de types de propriétés: ${widget.dataManager.propertyData.length}'); + print( + 'Nombre de types de propriétés: ${widget.dataManager.propertyData.length}'); for (var item in widget.dataManager.propertyData) { - print('Type de propriété: ${item['propertyType']}, Count: ${item['count']}'); + print( + 'Type de propriété: ${item['propertyType']}, Count: ${item['count']}'); } // Trier les données par 'count' dans l'ordre décroissant - final sortedData = List>.from(widget.dataManager.propertyData) - ..sort((a, b) => b['count'].compareTo(a['count'])); + final sortedData = + List>.from(widget.dataManager.propertyData) + ..sort((a, b) => b['count'].compareTo(a['count'])); // Calculer le total pour le pourcentage - final totalCount = widget.dataManager.propertyData.fold(0.0, (double sum, item) { + final totalCount = + widget.dataManager.propertyData.fold(0.0, (double sum, item) { final count = item['count'] ?? 0.0; return sum + (count is String ? double.tryParse(count) ?? 0.0 : count); }); @@ -176,15 +184,17 @@ class _TokenDistributionCardState extends State { return PieChartSectionData( value: data['count'].toDouble(), title: percentage < 5 ? '' : '${percentage.toStringAsFixed(1)}%', - color: baseColor.withOpacity(opacity), + color: baseColor.withValues(alpha: opacity), radius: isSelected ? 52 : 45, titleStyle: TextStyle( - fontSize: isSelected ? 14 + appState.getTextSizeOffset() : 10 + appState.getTextSizeOffset(), + fontSize: isSelected + ? 14 + appState.getTextSizeOffset() + : 10 + appState.getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -209,7 +219,7 @@ class _TokenDistributionCardState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: const Offset(0, 1), ), @@ -220,7 +230,8 @@ class _TokenDistributionCardState extends State { Widget _buildCenterTextToken(int? selectedIndex) { // Somme des valeurs de 'count' - final totalCount = widget.dataManager.propertyData.fold(0.0, (double sum, item) { + final totalCount = + widget.dataManager.propertyData.fold(0.0, (double sum, item) { final count = item['count'] ?? 0.0; // Utiliser 0.0 si 'count' est null return sum + (count is String ? double.tryParse(count) ?? 0.0 : count); }); @@ -252,11 +263,12 @@ class _TokenDistributionCardState extends State { } // Afficher les détails du segment sélectionné - final sortedData = List>.from(widget.dataManager.propertyData) - ..sort((a, b) => b['count'].compareTo(a['count'])); + final sortedData = + List>.from(widget.dataManager.propertyData) + ..sort((a, b) => b['count'].compareTo(a['count'])); if (selectedIndex >= sortedData.length) return Container(); - + final selectedData = sortedData[selectedIndex]; return Column( @@ -287,8 +299,9 @@ class _TokenDistributionCardState extends State { final appState = Provider.of(context); // Trier les données par 'count' dans l'ordre décroissant - final sortedData = List>.from(widget.dataManager.propertyData) - ..sort((a, b) => b['count'].compareTo(a['count'])); + final sortedData = + List>.from(widget.dataManager.propertyData) + ..sort((a, b) => b['count'].compareTo(a['count'])); return Wrap( spacing: 8.0, @@ -301,16 +314,21 @@ class _TokenDistributionCardState extends State { return InkWell( onTap: () { - _selectedIndexNotifierToken.value = (_selectedIndexNotifierToken.value == index) ? null : index; + _selectedIndexNotifierToken.value = + (_selectedIndexNotifierToken.value == index) ? null : index; }, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: _selectedIndexNotifierToken.value == index ? color.withOpacity(0.1) : Colors.transparent, + color: _selectedIndexNotifierToken.value == index + ? color.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: _selectedIndexNotifierToken.value == index ? color : Colors.transparent, + color: _selectedIndexNotifierToken.value == index + ? color + : Colors.transparent, width: 1, ), ), @@ -325,7 +343,7 @@ class _TokenDistributionCardState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ) @@ -335,11 +353,16 @@ class _TokenDistributionCardState extends State { const SizedBox(width: 8), Flexible( child: Text( - Parameters.getPropertyTypeName(data['propertyType'], context), + Parameters.getPropertyTypeName( + data['propertyType'], context), style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: _selectedIndexNotifierToken.value == index ? color : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: _selectedIndexNotifierToken.value == index ? FontWeight.w600 : FontWeight.normal, + color: _selectedIndexNotifierToken.value == index + ? color + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: _selectedIndexNotifierToken.value == index + ? FontWeight.w600 + : FontWeight.normal, ), ), ), diff --git a/lib/pages/Statistics/portfolio/portfolio_stats.dart b/lib/pages/Statistics/portfolio/portfolio_stats.dart index 6647e93..48f82e6 100644 --- a/lib/pages/Statistics/portfolio/portfolio_stats.dart +++ b/lib/pages/Statistics/portfolio/portfolio_stats.dart @@ -27,7 +27,8 @@ class PortfolioStats extends StatefulWidget { class _PortfolioStats extends State { late String _selectedPeriod; late String _selectedFilter; - bool _rentedIsBarChart = true; // Ajoutez cette variable pour gérer le type de graphique + bool _rentedIsBarChart = + true; // Ajoutez cette variable pour gérer le type de graphique @override void initState() { @@ -37,9 +38,11 @@ class _PortfolioStats extends State { try { // Vérifier si les données sont déjà disponibles final dataManager = Provider.of(context, listen: false); - - if (!dataManager.isLoadingMain && dataManager.evmAddresses.isNotEmpty && - dataManager.portfolio.isNotEmpty && dataManager.rentHistory.isNotEmpty) { + + if (!dataManager.isLoadingMain && + dataManager.evmAddresses.isNotEmpty && + dataManager.portfolio.isNotEmpty && + dataManager.rentHistory.isNotEmpty) { debugPrint("📊 Stats: données déjà chargées, skip chargement"); } else { debugPrint("📊 Stats: chargement des données nécessaire"); @@ -62,12 +65,6 @@ class _PortfolioStats extends State { Widget build(BuildContext context) { return Consumer( builder: (context, dataManager, child) { - if (dataManager == null) { - return const Scaffold( - body: Center(child: Text("Error loading data")), - ); - } - final screenWidth = MediaQuery.of(context).size.width; final isWideScreen = screenWidth > 700; final double fixedCardHeight = 380; @@ -79,7 +76,9 @@ class _PortfolioStats extends State { end: Alignment.bottomCenter, colors: [ Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9), + Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.9), ], ), ), @@ -87,7 +86,8 @@ class _PortfolioStats extends State { physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( - padding: const EdgeInsets.only(top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only( + top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: isWideScreen ? 2 : 1, @@ -99,7 +99,7 @@ class _PortfolioStats extends State { (BuildContext context, int index) { // Vérification de sécurité pour éviter les erreurs de renderObject if (!mounted) return const SizedBox.shrink(); - + return _buildChartWidget(context, index, dataManager); }, childCount: 12, @@ -113,7 +113,8 @@ class _PortfolioStats extends State { ); } - Widget _buildChartWidget(BuildContext context, int index, DataManager dataManager) { + Widget _buildChartWidget( + BuildContext context, int index, DataManager dataManager) { try { switch (index) { case 0: @@ -196,7 +197,7 @@ class _PortfolioStats extends State { } } catch (e) { debugPrint("Error building chart widget at index $index: $e"); - return Container( + return SizedBox( height: 380, child: const Center( child: Text("Erreur de chargement du graphique"), diff --git a/lib/pages/Statistics/rents/rents_stats.dart b/lib/pages/Statistics/rents/rents_stats.dart index 18a0ae6..154d5f8 100644 --- a/lib/pages/Statistics/rents/rents_stats.dart +++ b/lib/pages/Statistics/rents/rents_stats.dart @@ -21,15 +21,15 @@ class RentsStats extends StatefulWidget { class _RentsStatsState extends State { late String _selectedPeriod; late String _selectedRentPeriod; - + String _selectedRentTimeRange = 'all'; - + bool _rentedIsBarChart = true; bool rentIsBarChart = false; bool showCumulativeRent = false; - + late SharedPreferences prefs; - + int _rentTimeOffset = 0; @override @@ -43,22 +43,25 @@ class _RentsStatsState extends State { rentIsBarChart = prefs.getBool('rentIsBarChart') ?? false; showCumulativeRent = prefs.getBool('showCumulativeRent') ?? false; _rentedIsBarChart = prefs.getBool('rentedIsBarChart') ?? true; - + // Charger les périodes sélectionnées - _selectedRentPeriod = prefs.getString('rentPeriod') ?? S.of(context).week; - + _selectedRentPeriod = + prefs.getString('rentPeriod') ?? S.of(context).week; + // Charger les plages de temps _selectedRentTimeRange = prefs.getString('rentTimeRange') ?? 'all'; - + // Charger les offsets _rentTimeOffset = prefs.getInt('rentTimeOffset') ?? 0; }); try { final dataManager = Provider.of(context, listen: false); - - if (!dataManager.isLoadingMain && dataManager.evmAddresses.isNotEmpty && - dataManager.portfolio.isNotEmpty && dataManager.rentHistory.isNotEmpty) { + + if (!dataManager.isLoadingMain && + dataManager.evmAddresses.isNotEmpty && + dataManager.portfolio.isNotEmpty && + dataManager.rentHistory.isNotEmpty) { debugPrint("📊 RentsStats: données déjà chargées, skip chargement"); } else { debugPrint("📊 RentsStats: chargement des données nécessaire"); @@ -81,10 +84,6 @@ class _RentsStatsState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, dataManager, child) { - if (dataManager == null) { - return const Center(child: Text("Error loading data")); - } - final screenWidth = MediaQuery.of(context).size.width; final isWideScreen = screenWidth > 700; final double fixedCardHeight = 380; @@ -96,7 +95,9 @@ class _RentsStatsState extends State { end: Alignment.bottomCenter, colors: [ Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9), + Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.9), ], ), ), @@ -104,7 +105,8 @@ class _RentsStatsState extends State { physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( - padding: const EdgeInsets.only(top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only( + top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: isWideScreen ? 2 : 1, @@ -115,7 +117,7 @@ class _RentsStatsState extends State { delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (!mounted) return const SizedBox.shrink(); - + return _buildChartWidget(context, index, dataManager); }, childCount: 4, @@ -129,7 +131,8 @@ class _RentsStatsState extends State { ); } - Widget _buildChartWidget(BuildContext context, int index, DataManager dataManager) { + Widget _buildChartWidget( + BuildContext context, int index, DataManager dataManager) { try { switch (index) { case 0: @@ -211,7 +214,7 @@ class _RentsStatsState extends State { } } catch (e) { debugPrint("Error building chart widget at index $index: $e"); - return Container( + return SizedBox( height: 380, child: const Center( child: Text("Erreur de chargement du graphique"), @@ -235,4 +238,4 @@ class _RentsStatsState extends State { void _saveTimeOffsetPreference(String key, int value) { prefs.setInt(key, value); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/rmm/borrow_chart.dart b/lib/pages/Statistics/rmm/borrow_chart.dart index 0d270b0..ae7be7e 100644 --- a/lib/pages/Statistics/rmm/borrow_chart.dart +++ b/lib/pages/Statistics/rmm/borrow_chart.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/models/balance_record.dart'; -import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; -import 'package:realtoken_asset_tracker/utils/date_utils.dart'; import 'package:realtoken_asset_tracker/components/charts/generic_chart_widget.dart'; /// Classe représentant un groupe d'emprunts agrégé selon la période. @@ -81,7 +79,7 @@ class _BorrowChartState extends State { /// Convertit les historiques de balance en liste de BorrowRecord void _updateBorrowRecords() { final Map recordsMap = {}; - + // Traiter les emprunts USDC if (widget.allHistories['usdcBorrow'] != null) { for (final record in widget.allHistories['usdcBorrow']!) { @@ -101,7 +99,7 @@ class _BorrowChartState extends State { } } } - + // Traiter les emprunts xDai if (widget.allHistories['xdaiBorrow'] != null) { for (final record in widget.allHistories['xdaiBorrow']!) { @@ -121,7 +119,7 @@ class _BorrowChartState extends State { } } } - + // Convertir la Map en List et trier par date _borrowRecords = recordsMap.values.toList() ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); @@ -146,11 +144,11 @@ class _BorrowChartState extends State { @override Widget build(BuildContext context) { final currencyUtils = Provider.of(context, listen: false); - + // Deux couleurs pour les données empilées final Color primaryColor = const Color(0xFFFF9500); // Orange iOS pour USDC final Color secondaryColor = const Color(0xFFFF3B30); // Rouge iOS pour xDai - + return GenericChartWidget( title: S.of(context).borrowBalance, chartColor: primaryColor, diff --git a/lib/pages/Statistics/rmm/deposit_chart.dart b/lib/pages/Statistics/rmm/deposit_chart.dart index 4aa5bfe..5d433ed 100644 --- a/lib/pages/Statistics/rmm/deposit_chart.dart +++ b/lib/pages/Statistics/rmm/deposit_chart.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/models/balance_record.dart'; -import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; -import 'package:realtoken_asset_tracker/utils/date_utils.dart'; import 'package:realtoken_asset_tracker/components/charts/generic_chart_widget.dart'; /// Classe représentant un groupe de dépôts agrégé selon la période. @@ -81,7 +79,7 @@ class _DepositChartState extends State { /// Convertit les historiques de balance en liste de DepositRecord void _updateDepositRecords() { final Map recordsMap = {}; - + // Traiter les dépôts USDC if (widget.allHistories['usdcDeposit'] != null) { for (final record in widget.allHistories['usdcDeposit']!) { @@ -101,7 +99,7 @@ class _DepositChartState extends State { } } } - + // Traiter les dépôts xDai if (widget.allHistories['xdaiDeposit'] != null) { for (final record in widget.allHistories['xdaiDeposit']!) { @@ -121,7 +119,7 @@ class _DepositChartState extends State { } } } - + // Convertir la Map en List et trier par date _depositRecords = recordsMap.values.toList() ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); @@ -146,11 +144,11 @@ class _DepositChartState extends State { @override Widget build(BuildContext context) { final currencyUtils = Provider.of(context, listen: false); - + // Deux couleurs pour les données empilées final Color primaryColor = const Color(0xFF34C759); // Vert iOS pour USDC final Color secondaryColor = const Color(0xFF007AFF); // Bleu iOS pour xDai - + return GenericChartWidget( title: S.of(context).depositBalance, chartColor: primaryColor, diff --git a/lib/pages/Statistics/rmm/healthFactorLtv_graph.dart b/lib/pages/Statistics/rmm/healthFactorLtv_graph.dart index d3c9bef..cfc5810 100644 --- a/lib/pages/Statistics/rmm/healthFactorLtv_graph.dart +++ b/lib/pages/Statistics/rmm/healthFactorLtv_graph.dart @@ -25,7 +25,8 @@ class HealthAndLtvHistoryGraph extends StatefulWidget { }); @override - _HealthAndLtvHistoryGraphState createState() => _HealthAndLtvHistoryGraphState(); + _HealthAndLtvHistoryGraphState createState() => + _HealthAndLtvHistoryGraphState(); } class _HealthAndLtvHistoryGraphState extends State { @@ -72,7 +73,7 @@ class _HealthAndLtvHistoryGraphState extends State { return Card( elevation: 4, - shadowColor: Colors.black.withOpacity(0.1), + shadowColor: Colors.black.withValues(alpha: 0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), @@ -85,7 +86,7 @@ class _HealthAndLtvHistoryGraphState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.95), + Theme.of(context).cardColor.withValues(alpha: 0.95), ], ), ), @@ -107,9 +108,12 @@ class _HealthAndLtvHistoryGraphState extends State { ), const SizedBox(width: 16), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 0), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black12 : Colors.black.withOpacity(0.05), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black12 + : Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(20), ), child: Row( @@ -119,8 +123,11 @@ class _HealthAndLtvHistoryGraphState extends State { S.of(context).ltv, style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), - color: showHealthFactor ? Colors.grey : Colors.black, - fontWeight: showHealthFactor ? FontWeight.normal : FontWeight.w600, + color: + showHealthFactor ? Colors.grey : Colors.black, + fontWeight: showHealthFactor + ? FontWeight.normal + : FontWeight.w600, ), ), Transform.scale( @@ -134,8 +141,11 @@ class _HealthAndLtvHistoryGraphState extends State { }); }, activeColor: const Color(0xFF007AFF), // Bleu iOS - trackColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) ? Theme.of(context).primaryColor : Colors.grey.withOpacity(0.3), + trackColor: WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.selected) + ? Theme.of(context).primaryColor + : Colors.grey.withValues(alpha: 0.3), ), ), ), @@ -143,8 +153,11 @@ class _HealthAndLtvHistoryGraphState extends State { S.of(context).hf, style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), - color: showHealthFactor ? Colors.black : Colors.grey, - fontWeight: showHealthFactor ? FontWeight.w600 : FontWeight.normal, + color: + showHealthFactor ? Colors.black : Colors.grey, + fontWeight: showHealthFactor + ? FontWeight.w600 + : FontWeight.normal, ), ), ], @@ -154,22 +167,29 @@ class _HealthAndLtvHistoryGraphState extends State { // Bouton pour changer de type de graphique Container( decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black12 : Colors.black.withOpacity(0.05), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black12 + : Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: IconButton( icon: Icon( - widget.healthAndLtvIsBarChart ? Icons.show_chart : Icons.bar_chart, + widget.healthAndLtvIsBarChart + ? Icons.show_chart + : Icons.bar_chart, size: 20.0, color: const Color(0xFF007AFF), ), onPressed: () { - widget.onChartTypeChanged(!widget.healthAndLtvIsBarChart); + widget.onChartTypeChanged( + !widget.healthAndLtvIsBarChart); setState(() { _selectedSpotIndex = null; }); }, - tooltip: widget.healthAndLtvIsBarChart ? S.of(context).lineChart : S.of(context).barChart, + tooltip: widget.healthAndLtvIsBarChart + ? S.of(context).lineChart + : S.of(context).barChart, ), ), ], @@ -190,22 +210,33 @@ class _HealthAndLtvHistoryGraphState extends State { // Affichage du graphique (barres ou lignes) Expanded( child: widget.healthAndLtvIsBarChart - ? _buildBarChart(context, appState, groupedData, _buildDateLabelsForHealthAndLtv(context, widget.dataManager, widget.selectedPeriod)) - : _buildLineChart(context, appState, groupedData, spots, maxY), + ? _buildBarChart( + context, + appState, + groupedData, + _buildDateLabelsForHealthAndLtv(context, + widget.dataManager, widget.selectedPeriod)) + : _buildLineChart( + context, appState, groupedData, spots, maxY), ), // Indicateur de la métrique actuelle const SizedBox(height: 16), Center( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: showHealthFactor - ? const Color(0xFF34C759).withOpacity(0.1) // Vert iOS - : const Color(0xFFFF9500).withOpacity(0.1), // Orange iOS + ? const Color(0xFF34C759) + .withValues(alpha: 0.1) // Vert iOS + : const Color(0xFFFF9500) + .withValues(alpha: 0.1), // Orange iOS borderRadius: BorderRadius.circular(12), ), child: Text( - showHealthFactor ? S.of(context).healthFactorSafer : S.of(context).ltvSafer, + showHealthFactor + ? S.of(context).healthFactorSafer + : S.of(context).ltvSafer, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), color: showHealthFactor @@ -237,7 +268,7 @@ class _HealthAndLtvHistoryGraphState extends State { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, dashArray: [5, 5], ); @@ -255,7 +286,9 @@ class _HealthAndLtvHistoryGraphState extends State { final date = labels[groupIndex]; // Récupérer la valeur réelle pour l'affichage dans le tooltip - final realValue = showHealthFactor ? groupedData[date]!['healtFactor']! : groupedData[date]!['ltv']!; + final realValue = showHealthFactor + ? groupedData[date]!['healtFactor']! + : groupedData[date]!['ltv']!; return BarTooltipItem( '$date\n${showHealthFactor ? 'Health Factor' : 'LTV'}: ${showHealthFactor ? realValue.toInt() : realValue.toInt()}${showHealthFactor ? '' : '%'}', @@ -321,7 +354,9 @@ class _HealthAndLtvHistoryGraphState extends State { return Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - showHealthFactor ? value.toInt().toString() : '${value.toInt()}%', + showHealthFactor + ? value.toInt().toString() + : '${value.toInt()}%', style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey.shade600, @@ -342,7 +377,8 @@ class _HealthAndLtvHistoryGraphState extends State { final values = groupedData[date]; if (values == null) return BarChartGroupData(x: index); - double metricValue = showHealthFactor ? values['healtFactor']! : values['ltv']!; + double metricValue = + showHealthFactor ? values['healtFactor']! : values['ltv']!; // Assurer que la valeur de la barre est au moins 1.0 pour le graphique, // tout en conservant la valeur réelle pour l'affichage dans le tooltip @@ -355,16 +391,17 @@ class _HealthAndLtvHistoryGraphState extends State { x: index, barRods: [ BarChartRodData( - toY: barValue, // Utiliser la valeur ajustée qui est au moins 1.0 + toY: + barValue, // Utiliser la valeur ajustée qui est au moins 1.0 gradient: LinearGradient( colors: showHealthFactor ? [ const Color(0xFF34C759), // Vert iOS - const Color(0xFF34C759).withOpacity(0.7), + const Color(0xFF34C759).withValues(alpha: 0.7), ] : [ const Color(0xFFFF9500), // Orange iOS - const Color(0xFFFF9500).withOpacity(0.7), + const Color(0xFFFF9500).withValues(alpha: 0.7), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -377,7 +414,7 @@ class _HealthAndLtvHistoryGraphState extends State { backDrawRodData: BackgroundBarChartRodData( show: true, toY: showHealthFactor ? 3.0 : 100, - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withValues(alpha: 0.1), ), ), ], @@ -441,7 +478,7 @@ class _HealthAndLtvHistoryGraphState extends State { drawVerticalLine: false, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withOpacity(0.15), + color: Colors.grey.withValues(alpha: 0.15), strokeWidth: 1, dashArray: [5, 5], ); @@ -490,7 +527,9 @@ class _HealthAndLtvHistoryGraphState extends State { return Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - showHealthFactor ? value.toInt().toString() : '${value.toInt()}%', + showHealthFactor + ? value.toInt().toString() + : '${value.toInt()}%', style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey.shade600, @@ -529,7 +568,10 @@ class _HealthAndLtvHistoryGraphState extends State { }, checkToShowDot: (spot, barData) { // Afficher les points aux extrémités et quelques points intermédiaires - return spot.x == 0 || spot.x == barData.spots.length - 1 || spot.x % (barData.spots.length > 8 ? 4 : 2) == 0 || _selectedSpotIndex == spot.x.toInt(); + return spot.x == 0 || + spot.x == barData.spots.length - 1 || + spot.x % (barData.spots.length > 8 ? 4 : 2) == 0 || + _selectedSpotIndex == spot.x.toInt(); }, ), belowBarData: BarAreaData( @@ -537,12 +579,14 @@ class _HealthAndLtvHistoryGraphState extends State { gradient: LinearGradient( colors: showHealthFactor ? [ - const Color(0xFF34C759).withOpacity(0.3), // Vert iOS - const Color(0xFF34C759).withOpacity(0.0), + const Color(0xFF34C759) + .withValues(alpha: 0.3), // Vert iOS + const Color(0xFF34C759).withValues(alpha: 0.0), ] : [ - const Color(0xFFFF9500).withOpacity(0.3), // Orange iOS - const Color(0xFFFF9500).withOpacity(0.0), + const Color(0xFFFF9500) + .withValues(alpha: 0.3), // Orange iOS + const Color(0xFFFF9500).withValues(alpha: 0.0), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -568,7 +612,9 @@ class _HealthAndLtvHistoryGraphState extends State { final date = dates[spotIndex]; // Récupérer la valeur réelle pour l'affichage - final realValue = showHealthFactor ? groupedData[date]!['healtFactor']! : groupedData[date]!['ltv']!; + final realValue = showHealthFactor + ? groupedData[date]!['healtFactor']! + : groupedData[date]!['ltv']!; return [ LineTooltipItem( @@ -591,11 +637,14 @@ class _HealthAndLtvHistoryGraphState extends State { ), handleBuiltInTouches: true, touchSpotThreshold: 20, - touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { setState(() { if (event is FlTapUpEvent || event is FlPanDownEvent) { - if (touchResponse?.lineBarSpots != null && touchResponse!.lineBarSpots!.isNotEmpty) { - _selectedSpotIndex = touchResponse.lineBarSpots!.first.spotIndex; + if (touchResponse?.lineBarSpots != null && + touchResponse!.lineBarSpots!.isNotEmpty) { + _selectedSpotIndex = + touchResponse.lineBarSpots!.first.spotIndex; } } else if (event is FlPanEndEvent || event is FlLongPressEnd) { _selectedSpotIndex = null; @@ -622,7 +671,8 @@ class _HealthAndLtvHistoryGraphState extends State { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -649,7 +699,8 @@ class _HealthAndLtvHistoryGraphState extends State { DataManager dataManager, String selectedPeriod, ) { - final groupedData = _groupHealthAndLtvByDate(context, dataManager, selectedPeriod); + final groupedData = + _groupHealthAndLtvByDate(context, dataManager, selectedPeriod); return groupedData.keys.toList(); } } diff --git a/lib/pages/Statistics/rmm/rmm_stats.dart b/lib/pages/Statistics/rmm/rmm_stats.dart index 81d4b72..c375ff9 100644 --- a/lib/pages/Statistics/rmm/rmm_stats.dart +++ b/lib/pages/Statistics/rmm/rmm_stats.dart @@ -7,9 +7,7 @@ import 'package:realtoken_asset_tracker/models/balance_record.dart'; import 'package:realtoken_asset_tracker/pages/Statistics/rmm/borrow_chart.dart'; import 'package:realtoken_asset_tracker/pages/Statistics/rmm/deposit_chart.dart'; import 'package:realtoken_asset_tracker/pages/Statistics/rmm/healthFactorLtv_graph.dart'; -import 'package:realtoken_asset_tracker/utils/chart_utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:realtoken_asset_tracker/app_state.dart'; class RmmStats extends StatefulWidget { const RmmStats({super.key}); @@ -34,7 +32,7 @@ class RmmStatsState extends State { String selectedDepositTimeRange = 'all'; int depositTimeOffset = 0; bool isDepositBarChart = false; - + String selectedBorrowTimeRange = 'all'; int borrowTimeOffset = 0; bool isBorrowBarChart = false; @@ -50,17 +48,21 @@ class RmmStatsState extends State { setState(() { // Charger la préférence stockée, si elle existe, sinon utiliser true par défaut healthAndLtvIsBarChart = prefs.getBool('healthAndLtvIsBarChart') ?? true; - + // Initialiser les nouvelles options - selectedDepositPeriod = prefs.getString('selectedDepositPeriod') ?? S.of(context).month; - selectedBorrowPeriod = prefs.getString('selectedBorrowPeriod') ?? S.of(context).month; - + selectedDepositPeriod = + prefs.getString('selectedDepositPeriod') ?? S.of(context).month; + selectedBorrowPeriod = + prefs.getString('selectedBorrowPeriod') ?? S.of(context).month; + isDepositBarChart = prefs.getBool('isDepositBarChart') ?? false; - selectedDepositTimeRange = prefs.getString('selectedDepositTimeRange') ?? 'all'; + selectedDepositTimeRange = + prefs.getString('selectedDepositTimeRange') ?? 'all'; depositTimeOffset = prefs.getInt('depositTimeOffset') ?? 0; - + isBorrowBarChart = prefs.getBool('isBorrowBarChart') ?? false; - selectedBorrowTimeRange = prefs.getString('selectedBorrowTimeRange') ?? 'all'; + selectedBorrowTimeRange = + prefs.getString('selectedBorrowTimeRange') ?? 'all'; borrowTimeOffset = prefs.getInt('borrowTimeOffset') ?? 0; }); } @@ -88,40 +90,40 @@ class RmmStatsState extends State { const double fixedCardHeight = 380; return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: _buildApyCard(dataManager, screenWidth), - ), + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: _buildApyCard(dataManager, screenWidth), ), - SliverPadding( - padding: const EdgeInsets.only(top: 8, left: 8, right: 8, bottom: 80), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: screenWidth > 700 ? 2 : 1, - mainAxisExtent: fixedCardHeight, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - switch (index) { - case 0: - return _buildDepositBalanceCard(dataManager, 380); - case 1: - return _buildBorrowBalanceCard(dataManager, 380); - case 2: - return _buildHealthAndLtvHistoryCard(dataManager); - - default: - return Container(); - } - }, - childCount: 3, - ), + ), + SliverPadding( + padding: const EdgeInsets.only(top: 8, left: 8, right: 8, bottom: 80), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: screenWidth > 700 ? 2 : 1, + mainAxisExtent: fixedCardHeight, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + switch (index) { + case 0: + return _buildDepositBalanceCard(dataManager, 380); + case 1: + return _buildBorrowBalanceCard(dataManager, 380); + case 2: + return _buildHealthAndLtvHistoryCard(dataManager); + + default: + return Container(); + } + }, + childCount: 3, ), ), - ], - ); + ), + ], + ); } Widget _buildHealthAndLtvHistoryCard(DataManager dataManager) { @@ -154,7 +156,7 @@ class RmmStatsState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( - color: Theme.of(context).dividerColor.withOpacity(0.1), + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), width: 0.5, ), ), @@ -189,16 +191,20 @@ class RmmStatsState extends State { ); }, child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), + padding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 10), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - '${S.of(context).averageApy}', + S.of(context).averageApy, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -258,21 +264,26 @@ class RmmStatsState extends State { child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 6), - child: Image.asset('assets/icons/usdc.png', width: 22, height: 22), + child: Image.asset('assets/icons/usdc.png', + width: 22, height: 22), ), ), Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 6), - child: Image.asset('assets/icons/xdai.png', width: 22, height: 22), + child: Image.asset('assets/icons/xdai.png', + width: 22, height: 22), ), ), ], ), // Ligne de séparation - Divider(height: 0, thickness: 0.5, color: Theme.of(context).dividerColor.withOpacity(0.1)), + Divider( + height: 0, + thickness: 0.5, + color: Theme.of(context).dividerColor.withValues(alpha: 0.1)), // Ligne Deposit Row( @@ -280,7 +291,8 @@ class RmmStatsState extends State { SizedBox( width: 80, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 6.0), child: Text('Deposit', style: textStyle), ), ), @@ -312,7 +324,10 @@ class RmmStatsState extends State { ), // Ligne de séparation - Divider(height: 0, thickness: 0.5, color: Theme.of(context).dividerColor.withOpacity(0.1)), + Divider( + height: 0, + thickness: 0.5, + color: Theme.of(context).dividerColor.withValues(alpha: 0.1)), // Ligne Borrow Row( @@ -320,7 +335,8 @@ class RmmStatsState extends State { SizedBox( width: 80, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 6.0), child: Text('Borrow', style: textStyle), ), ), @@ -369,7 +385,8 @@ class RmmStatsState extends State { children: [ Expanded( child: FutureBuilder>>( - future: _fetchAndAggregateBalanceHistories(dataManager, selectedDepositPeriod), + future: _fetchAndAggregateBalanceHistories( + dataManager, selectedDepositPeriod), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); @@ -399,8 +416,10 @@ class RmmStatsState extends State { onTimeRangeChanged: (newRange) { setState(() { selectedDepositTimeRange = newRange; - depositTimeOffset = 0; // Réinitialiser l'offset lors du changement de plage - prefs.setString('selectedDepositTimeRange', newRange); + depositTimeOffset = + 0; // Réinitialiser l'offset lors du changement de plage + prefs.setString( + 'selectedDepositTimeRange', newRange); prefs.setInt('depositTimeOffset', 0); }); }, @@ -423,22 +442,29 @@ class RmmStatsState extends State { ); } - Future>> _fetchAndAggregateBalanceHistories(DataManager dataManager, String selectedPeriod) async { + Future>> _fetchAndAggregateBalanceHistories( + DataManager dataManager, String selectedPeriod) async { Map> allHistories = {}; - allHistories['usdcDeposit'] = await _archiveManager.getBalanceHistory('usdcDeposit'); - allHistories['usdcBorrow'] = await _archiveManager.getBalanceHistory('usdcBorrow'); - allHistories['xdaiDeposit'] = await _archiveManager.getBalanceHistory('xdaiDeposit'); - allHistories['xdaiBorrow'] = await _archiveManager.getBalanceHistory('xdaiBorrow'); + allHistories['usdcDeposit'] = + await _archiveManager.getBalanceHistory('usdcDeposit'); + allHistories['usdcBorrow'] = + await _archiveManager.getBalanceHistory('usdcBorrow'); + allHistories['xdaiDeposit'] = + await _archiveManager.getBalanceHistory('xdaiDeposit'); + allHistories['xdaiBorrow'] = + await _archiveManager.getBalanceHistory('xdaiBorrow'); for (String tokenType in allHistories.keys) { - allHistories[tokenType] = await _aggregateByPeriod(allHistories[tokenType]!, selectedPeriod); + allHistories[tokenType] = + await _aggregateByPeriod(allHistories[tokenType]!, selectedPeriod); } return allHistories; } - Future> _aggregateByPeriod(List records, String period) async { + Future> _aggregateByPeriod( + List records, String period) async { Map> groupedByPeriod = {}; for (var record in records) { @@ -476,12 +502,15 @@ class RmmStatsState extends State { ); } - groupedByPeriod.putIfAbsent(truncatedToPeriod, () => []).add(record.balance); + groupedByPeriod + .putIfAbsent(truncatedToPeriod, () => []) + .add(record.balance); } List averagedRecords = []; groupedByPeriod.forEach((timestamp, balances) { - double averageBalance = balances.reduce((a, b) => a + b) / balances.length; + double averageBalance = + balances.reduce((a, b) => a + b) / balances.length; averagedRecords.add(BalanceRecord( tokenType: records.first.tokenType, balance: averageBalance, @@ -506,7 +535,8 @@ class RmmStatsState extends State { children: [ Expanded( child: FutureBuilder>>( - future: _fetchAndAggregateBalanceHistories(dataManager, selectedBorrowPeriod), + future: _fetchAndAggregateBalanceHistories( + dataManager, selectedBorrowPeriod), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); @@ -536,8 +566,10 @@ class RmmStatsState extends State { onTimeRangeChanged: (newRange) { setState(() { selectedBorrowTimeRange = newRange; - borrowTimeOffset = 0; // Réinitialiser l'offset lors du changement de plage - prefs.setString('selectedBorrowTimeRange', newRange); + borrowTimeOffset = + 0; // Réinitialiser l'offset lors du changement de plage + prefs.setString( + 'selectedBorrowTimeRange', newRange); prefs.setInt('borrowTimeOffset', 0); }); }, diff --git a/lib/pages/Statistics/stats_selector_page.dart b/lib/pages/Statistics/stats_selector_page.dart index 69264d1..65db290 100644 --- a/lib/pages/Statistics/stats_selector_page.dart +++ b/lib/pages/Statistics/stats_selector_page.dart @@ -14,7 +14,8 @@ class StatsSelectorPage extends StatefulWidget { StatsSelectorPageState createState() => StatsSelectorPageState(); } -class StatsSelectorPageState extends State with TickerProviderStateMixin { +class StatsSelectorPageState extends State + with TickerProviderStateMixin { String _selectedStats = 'WalletStats'; String _previousSelectedStats = 'WalletStats'; @@ -28,7 +29,7 @@ class StatsSelectorPageState extends State with TickerProvide // Contrôleurs d'animation pour chaque sélecteur final Map _animationControllers = {}; final Map> _scaleAnimations = {}; - + // Contrôleurs pour l'animation du sélecteur late AnimationController _selectorAnimationController; late Animation _selectorAnimation; @@ -69,7 +70,7 @@ class StatsSelectorPageState extends State with TickerProvide duration: const Duration(milliseconds: 300), value: 1.0, ); - + _selectorAnimation = Tween( begin: 0.0, end: 1.0, @@ -106,7 +107,7 @@ class StatsSelectorPageState extends State with TickerProvide void _handleScroll(double offset) { const double threshold = 50.0; // Seuil de déclenchement - + if (offset > _lastScrollOffset + threshold && _isSelectorVisible) { // Scroll vers le bas - masquer le sélecteur setState(() { @@ -120,7 +121,7 @@ class StatsSelectorPageState extends State with TickerProvide }); _selectorAnimationController.forward(); } - + _lastScrollOffset = offset; } @@ -139,7 +140,8 @@ class StatsSelectorPageState extends State with TickerProvide body: Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { return [ AnimatedBuilder( animation: _selectorAnimation, @@ -147,26 +149,33 @@ class StatsSelectorPageState extends State with TickerProvide return SliverAppBar( floating: true, snap: true, - expandedHeight: (UIUtils.getSliverAppBarHeight(context) + 10) * _selectorAnimation.value, + expandedHeight: + (UIUtils.getSliverAppBarHeight(context) + 10) * + _selectorAnimation.value, collapsedHeight: _selectorAnimation.value == 0 ? 0 : null, - toolbarHeight: _selectorAnimation.value == 0 ? 0 : kToolbarHeight, - flexibleSpace: _selectorAnimation.value > 0 ? FlexibleSpaceBar( - background: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Opacity( - opacity: _selectorAnimation.value, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: _buildStatsSelector(), + toolbarHeight: + _selectorAnimation.value == 0 ? 0 : kToolbarHeight, + flexibleSpace: _selectorAnimation.value > 0 + ? FlexibleSpaceBar( + background: Container( + color: + Theme.of(context).scaffoldBackgroundColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Opacity( + opacity: _selectorAnimation.value, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), + child: _buildStatsSelector(), + ), + ), + ], ), ), - ], - ), - ), - ) : null, + ) + : null, ); }, ), @@ -197,7 +206,8 @@ class StatsSelectorPageState extends State with TickerProvide Widget _buildStatsSelector() { return Row( children: [ - _buildStatsChip('WalletStats', S.of(context).wallet, Icons.account_balance_wallet), + _buildStatsChip( + 'WalletStats', S.of(context).wallet, Icons.account_balance_wallet), _buildStatsChip('RentsStats', S.of(context).rents, Icons.attach_money), _buildStatsChip('RMMStats', S.of(context).rmm, Icons.money), ], @@ -215,7 +225,9 @@ class StatsSelectorPageState extends State with TickerProvide fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ); - double minWidth = isSelected ? _calculateTextWidth(context, label, textStyle) : 56; // Largeur minimale pour les icônes non sélectionnées + double minWidth = isSelected + ? _calculateTextWidth(context, label, textStyle) + : 56; // Largeur minimale pour les icônes non sélectionnées // Utiliser l'animation d'échelle si disponible Widget animatedContent = _scaleAnimations.containsKey(value) @@ -318,14 +330,16 @@ class StatsSelectorPageState extends State with TickerProvide ); } - double _calculateTextWidth(BuildContext context, String text, TextStyle style) { + double _calculateTextWidth( + BuildContext context, String text, TextStyle style) { final TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr, )..layout(); - return textPainter.width + 24; // Ajout de padding pour éviter que le texte touche les bords + return textPainter.width + + 24; // Ajout de padding pour éviter que le texte touche les bords } Widget _getSelectedStatsPage() { diff --git a/lib/pages/Statistics/wallet/charts/apy_graph.dart b/lib/pages/Statistics/wallet/charts/apy_graph.dart index 1b1be34..ca0ddb5 100644 --- a/lib/pages/Statistics/wallet/charts/apy_graph.dart +++ b/lib/pages/Statistics/wallet/charts/apy_graph.dart @@ -14,12 +14,8 @@ class EditableAPYRecord { final TextEditingController grossController; final TextEditingController dateController; - EditableAPYRecord( - this.original, - this.netController, - this.grossController, - this.dateController - ); + EditableAPYRecord(this.original, this.netController, this.grossController, + this.dateController); } class ApyHistoryGraph extends StatefulWidget { @@ -67,26 +63,33 @@ class _ApyHistoryGraphState extends State { return records.map((record) { return EditableAPYRecord( record, - TextEditingController(text: (record.netApy ?? record.apy).toStringAsFixed(2)), - TextEditingController(text: (record.grossApy ?? record.apy).toStringAsFixed(2)), - TextEditingController(text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), + TextEditingController( + text: (record.netApy ?? record.apy).toStringAsFixed(2)), + TextEditingController( + text: (record.grossApy ?? record.apy).toStringAsFixed(2)), + TextEditingController( + text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), ); }).toList(); } - void _updateAPYValue(DataManager dataManager, EditableAPYRecord editableRecord, + void _updateAPYValue( + DataManager dataManager, EditableAPYRecord editableRecord, {double? netValue, double? grossValue}) { // Récupérer l'index de l'enregistrement original - final index = dataManager.apyHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.apy == editableRecord.original.apy - ); - + final index = dataManager.apyHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.apy == editableRecord.original.apy); + if (index != -1) { // Déterminer les valeurs à utiliser - final newNetValue = netValue ?? editableRecord.original.netApy ?? editableRecord.original.apy; - final newGrossValue = grossValue ?? editableRecord.original.grossApy ?? editableRecord.original.apy; - + final newNetValue = netValue ?? + editableRecord.original.netApy ?? + editableRecord.original.apy; + final newGrossValue = grossValue ?? + editableRecord.original.grossApy ?? + editableRecord.original.apy; + // Créer un nouvel enregistrement avec les valeurs mises à jour final updatedRecord = APYRecord( timestamp: editableRecord.original.timestamp, @@ -94,18 +97,18 @@ class _ApyHistoryGraphState extends State { netApy: newNetValue, grossApy: newGrossValue, ); - + // Mettre à jour la liste dataManager.apyHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveApyHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour les contrôleurs si nécessaire if (netValue != null) { editableRecord.netController.text = newNetValue.toStringAsFixed(2); @@ -113,21 +116,22 @@ class _ApyHistoryGraphState extends State { if (grossValue != null) { editableRecord.grossController.text = newGrossValue.toStringAsFixed(2); } - + // Pour le débogage - print('APY mis à jour à l\'index $index: Net=$newNetValue, Gross=$newGrossValue'); + print( + 'APY mis à jour à l\'index $index: Net=$newNetValue, Gross=$newGrossValue'); } else { print('Enregistrement non trouvé pour la mise à jour'); } } - void _updateAPYDate(DataManager dataManager, EditableAPYRecord editableRecord, DateTime newDate) { + void _updateAPYDate(DataManager dataManager, EditableAPYRecord editableRecord, + DateTime newDate) { // Récupérer l'index de l'enregistrement original - final index = dataManager.apyHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.apy == editableRecord.original.apy - ); - + final index = dataManager.apyHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.apy == editableRecord.original.apy); + if (index != -1) { // Créer un nouvel enregistrement avec la date mise à jour final updatedRecord = APYRecord( @@ -136,53 +140,55 @@ class _ApyHistoryGraphState extends State { netApy: editableRecord.original.netApy, grossApy: editableRecord.original.grossApy, ); - + // Mettre à jour la liste dataManager.apyHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveApyHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour l'enregistrement éditable - editableRecord.dateController.text = DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); - + editableRecord.dateController.text = + DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); + // Pour le débogage - print('Date APY mise à jour à l\'index $index: ${newDate.toIso8601String()}'); + print( + 'Date APY mise à jour à l\'index $index: ${newDate.toIso8601String()}'); } else { print('Enregistrement non trouvé pour la mise à jour de la date'); } } - void _deleteAPYRecord(DataManager dataManager, EditableAPYRecord editableRecord, StateSetter setState) { + void _deleteAPYRecord(DataManager dataManager, + EditableAPYRecord editableRecord, StateSetter setState) { // Récupérer l'index de l'enregistrement original - final index = dataManager.apyHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.apy == editableRecord.original.apy - ); - + final index = dataManager.apyHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.apy == editableRecord.original.apy); + if (index != -1) { // Supprimer de la liste dataManager.apyHistory.removeAt(index); - + // Sauvegarder dans Hive dataManager.saveApyHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Pour le débogage print('APY supprimé à l\'index $index'); - + // Recréer les enregistrements éditables setState(() { _editableRecords = _createEditableRecords( - List.from(dataManager.apyHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(dataManager.apyHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); }); } else { print('Enregistrement non trouvé pour la suppression'); @@ -192,8 +198,8 @@ class _ApyHistoryGraphState extends State { void _showEditModal(BuildContext context) { // Créer des enregistrements éditables à partir des enregistrements triés _editableRecords = _createEditableRecords( - List.from(widget.dataManager.apyHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(widget.dataManager.apyHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); showModalBottomSheet( context: context, @@ -215,7 +221,7 @@ class _ApyHistoryGraphState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -1), ), @@ -231,7 +237,9 @@ class _ApyHistoryGraphState extends State { Text( 'Éditer l\'historique APY', style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -250,7 +258,10 @@ class _ApyHistoryGraphState extends State { child: Text( "Aucun historique disponible", style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -269,7 +280,10 @@ class _ApyHistoryGraphState extends State { "Date", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -281,7 +295,10 @@ class _ApyHistoryGraphState extends State { S.of(context).net, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -293,7 +310,10 @@ class _ApyHistoryGraphState extends State { S.of(context).brute, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -305,7 +325,10 @@ class _ApyHistoryGraphState extends State { "Actions", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), textAlign: TextAlign.right, ), @@ -319,59 +342,91 @@ class _ApyHistoryGraphState extends State { SizedBox( width: 150, child: TextField( - controller: editableRecord.dateController, - keyboardType: TextInputType.datetime, - textInputAction: TextInputAction.done, + controller: + editableRecord.dateController, + keyboardType: + TextInputType.datetime, + textInputAction: + TextInputAction.done, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(value); - _updateAPYDate(widget.dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(value); + _updateAPYDate( + widget.dataManager, + editableRecord, + newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, onEditingComplete: () { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(editableRecord.dateController.text); - _updateAPYDate(widget.dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(editableRecord + .dateController.text); + _updateAPYDate( + widget.dataManager, + editableRecord, + newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, @@ -382,70 +437,94 @@ class _ApyHistoryGraphState extends State { SizedBox( width: 100, child: TextField( - controller: editableRecord.netController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - textInputAction: TextInputAction.done, + controller: + editableRecord.netController, + keyboardType: const TextInputType + .numberWithOptions( + decimal: true), + textInputAction: + TextInputAction.done, inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d*')), ], style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { - double? newValue = double.tryParse(value); + double? newValue = + double.tryParse(value); if (newValue != null) { _updateAPYValue( - widget.dataManager, - editableRecord, - netValue: newValue - ); - FocusScope.of(context).unfocus(); + widget.dataManager, + editableRecord, + netValue: newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: $value'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: $value'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, onEditingComplete: () { - double? newValue = double.tryParse(editableRecord.netController.text); + double? newValue = + double.tryParse(editableRecord + .netController.text); if (newValue != null) { _updateAPYValue( - widget.dataManager, - editableRecord, - netValue: newValue - ); - FocusScope.of(context).unfocus(); + widget.dataManager, + editableRecord, + netValue: newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: ${editableRecord.netController.text}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: ${editableRecord.netController.text}'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, @@ -456,70 +535,94 @@ class _ApyHistoryGraphState extends State { SizedBox( width: 100, child: TextField( - controller: editableRecord.grossController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - textInputAction: TextInputAction.done, + controller: + editableRecord.grossController, + keyboardType: const TextInputType + .numberWithOptions( + decimal: true), + textInputAction: + TextInputAction.done, inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d*')), ], style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { - double? newValue = double.tryParse(value); + double? newValue = + double.tryParse(value); if (newValue != null) { _updateAPYValue( - widget.dataManager, - editableRecord, - grossValue: newValue - ); - FocusScope.of(context).unfocus(); + widget.dataManager, + editableRecord, + grossValue: newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: $value'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: $value'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, onEditingComplete: () { - double? newValue = double.tryParse(editableRecord.grossController.text); + double? newValue = + double.tryParse(editableRecord + .grossController.text); if (newValue != null) { _updateAPYValue( - widget.dataManager, - editableRecord, - grossValue: newValue - ); - FocusScope.of(context).unfocus(); + widget.dataManager, + editableRecord, + grossValue: newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: ${editableRecord.grossController.text}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: ${editableRecord.grossController.text}'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, @@ -530,7 +633,8 @@ class _ApyHistoryGraphState extends State { SizedBox( width: 60, child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ IconButton( icon: Icon( @@ -539,7 +643,10 @@ class _ApyHistoryGraphState extends State { size: 20, ), onPressed: () { - _deleteAPYRecord(widget.dataManager, editableRecord, setState); + _deleteAPYRecord( + widget.dataManager, + editableRecord, + setState); }, ), ], @@ -563,52 +670,64 @@ class _ApyHistoryGraphState extends State { for (var editableRecord in _editableRecords) { try { // Mettre à jour la date - final dateText = editableRecord.dateController.text; - final DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(dateText); - + final dateText = + editableRecord.dateController.text; + final DateTime newDate = + DateFormat('yyyy-MM-dd HH:mm:ss') + .parse(dateText); + // Mettre à jour la valeur nette final netText = editableRecord.netController.text; - final double? newNetValue = double.tryParse(netText); - + final double? newNetValue = + double.tryParse(netText); + // Mettre à jour la valeur brute - final grossText = editableRecord.grossController.text; - final double? newGrossValue = double.tryParse(grossText); - - if (newNetValue != null && newGrossValue != null) { + final grossText = + editableRecord.grossController.text; + final double? newGrossValue = + double.tryParse(grossText); + + if (newNetValue != null && + newGrossValue != null) { // Trouver l'index dans la liste originale - final index = widget.dataManager.apyHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.apy == editableRecord.original.apy - ); - + final index = widget.dataManager.apyHistory + .indexWhere((r) => + r.timestamp.isAtSameMomentAs( + editableRecord + .original.timestamp) && + r.apy == editableRecord.original.apy); + if (index != -1) { // Créer un nouvel enregistrement avec les nouvelles valeurs final updatedRecord = APYRecord( timestamp: newDate, - apy: newNetValue, // Utiliser la valeur nette comme valeur principale + apy: + newNetValue, // Utiliser la valeur nette comme valeur principale netApy: newNetValue, grossApy: newGrossValue, ); - + // Mettre à jour la liste - widget.dataManager.apyHistory[index] = updatedRecord; - print('Mise à jour index $index: ${newDate.toIso8601String()} -> $newNetValue'); + widget.dataManager.apyHistory[index] = + updatedRecord; + print( + 'Mise à jour index $index: ${newDate.toIso8601String()} -> $newNetValue'); } } } catch (e) { print('Erreur lors de la mise à jour: $e'); } } - + // Sauvegarder dans Hive widget.dataManager.saveApyHistory(); - + // Notifier les écouteurs widget.dataManager.notifyListeners(); - + // Fermer le modal Navigator.pop(context); - + // Forcer la mise à jour du widget parent setState(() {}); }, @@ -624,7 +743,9 @@ class _ApyHistoryGraphState extends State { child: Text( 'Sauvegarder', style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -645,7 +766,7 @@ class _ApyHistoryGraphState extends State { // Définir les couleurs pour les séries empilées final Color netColor = const Color(0xFF5AC8FA); // Bleu pour net final Color grossColor = const Color(0xFFFF9500); // Orange pour brut - + return GenericChartWidget( title: S.of(context).apyHistory, chartColor: netColor, // Utiliser la couleur de la série principale @@ -659,23 +780,26 @@ class _ApyHistoryGraphState extends State { onTimeRangeChanged: widget.onTimeRangeChanged, timeOffset: widget.timeOffset, onTimeOffsetChanged: widget.onTimeOffsetChanged, - getYValue: (record) => record?.apy ?? 0.0, + getYValue: (record) => record.apy, // Fournir les valeurs pour l'empilement getStackValues: (record) { - final netValue = record?.netApy ?? record?.apy ?? 0.0; - final grossValue = record?.grossApy ?? record?.apy ?? 0.0; - + final netValue = record.netApy ?? record.apy; + final grossValue = record.grossApy ?? record.apy; + // Si gross est inférieur à net (rare), on affiche juste la valeur nette final grossDiff = grossValue > netValue ? grossValue - netValue : 0.0; - + return [netValue, grossDiff]; }, - getTimestamp: (record) => record?.timestamp ?? DateTime.now(), + getTimestamp: (record) => record.timestamp, valuePrefix: '', valueSuffix: '%', maxY: 20, // Limiter la hauteur à 20% isStacked: true, // Activer l'affichage empilé pour APY - stackLabels: [S.of(context).net, S.of(context).brute], // Ajouter les étiquettes pour la légende + stackLabels: [ + S.of(context).net, + S.of(context).brute + ], // Ajouter les étiquettes pour la légende onEditPressed: (context) => _showEditModal(context), ); } diff --git a/lib/pages/Statistics/wallet/charts/rent_distribution_by_wallet_chart.dart b/lib/pages/Statistics/wallet/charts/rent_distribution_by_wallet_chart.dart index d113ea4..b400f89 100644 --- a/lib/pages/Statistics/wallet/charts/rent_distribution_by_wallet_chart.dart +++ b/lib/pages/Statistics/wallet/charts/rent_distribution_by_wallet_chart.dart @@ -12,10 +12,12 @@ class RentDistributionByWalletChart extends StatefulWidget { const RentDistributionByWalletChart({super.key, required this.dataManager}); @override - _RentDistributionByWalletChartState createState() => _RentDistributionByWalletChartState(); + _RentDistributionByWalletChartState createState() => + _RentDistributionByWalletChartState(); } -class _RentDistributionByWalletChartState extends State { +class _RentDistributionByWalletChartState + extends State { int? _selectedIndex; final ValueNotifier _selectedIndexNotifier = ValueNotifier(null); @@ -58,7 +60,7 @@ class _RentDistributionByWalletChartState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterText(selectedIndex), @@ -145,7 +152,8 @@ class _RentDistributionByWalletChartState extends State sum + value); + final double totalRent = + walletRentTotals.values.fold(0.0, (sum, value) => sum + value); // Vérifier si le total est valide if (totalRent <= 0) { @@ -153,43 +161,52 @@ class _RentDistributionByWalletChartState extends State b.value.compareTo(a.value)); + final sortedEntries = walletRentTotals.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); // Créer les sections du donut chart avec des vérifications de sécurité - return sortedEntries.asMap().entries.map((entry) { - final index = entry.key; - final data = entry.value; - - // Vérifier si la valeur est valide - if (data.value <= 0) { - return null; - } - - final percentage = (data.value / totalRent) * 100; - final bool isSelected = selectedIndex == index; - final double opacity = selectedIndex != null && !isSelected ? 0.5 : 1.0; - - return PieChartSectionData( - value: data.value, - title: '${percentage.toInt()}%', - color: _generateColor(index).withOpacity(opacity), - radius: isSelected ? 52 : 45, - titleStyle: TextStyle( - fontSize: isSelected ? 14 + Provider.of(context).getTextSizeOffset() : 10 + Provider.of(context).getTextSizeOffset(), - color: Colors.white, - fontWeight: FontWeight.w600, - shadows: [ - Shadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 3, - offset: const Offset(1, 1), + return sortedEntries + .asMap() + .entries + .map((entry) { + final index = entry.key; + final data = entry.value; + + // Vérifier si la valeur est valide + if (data.value <= 0) { + return null; + } + + final percentage = (data.value / totalRent) * 100; + final bool isSelected = selectedIndex == index; + final double opacity = + selectedIndex != null && !isSelected ? 0.5 : 1.0; + + return PieChartSectionData( + value: data.value, + title: '${percentage.toInt()}%', + color: _generateColor(index).withValues(alpha: opacity), + radius: isSelected ? 52 : 45, + titleStyle: TextStyle( + fontSize: isSelected + ? 14 + Provider.of(context).getTextSizeOffset() + : 10 + Provider.of(context).getTextSizeOffset(), + color: Colors.white, + fontWeight: FontWeight.w600, + shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 3, + offset: const Offset(1, 1), + ), + ], ), - ], - ), - badgeWidget: isSelected ? _buildSelectedIndicator() : null, - badgePositionPercentageOffset: 1.1, - ); - }).whereType().toList(); + badgeWidget: isSelected ? _buildSelectedIndicator() : null, + badgePositionPercentageOffset: 1.1, + ); + }) + .whereType() + .toList(); } Widget _buildSelectedIndicator() { @@ -205,7 +222,7 @@ class _RentDistributionByWalletChartState extends State walletRentTotals = _calculateWalletRentTotals(); - final double totalRent = walletRentTotals.values.fold(0.0, (sum, value) => sum + value); + final double totalRent = + walletRentTotals.values.fold(0.0, (sum, value) => sum + value); final currencyUtils = Provider.of(context, listen: false); if (selectedIndex == null) { @@ -247,7 +265,8 @@ class _RentDistributionByWalletChartState extends State b.value.compareTo(a.value)); + final sortedEntries = walletRentTotals.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); final selectedEntry = sortedEntries[selectedIndex]; // Raccourcir l'adresse du wallet pour l'affichage @@ -284,7 +303,8 @@ class _RentDistributionByWalletChartState extends State walletRentTotals = _calculateWalletRentTotals(); final appState = Provider.of(context); - final sortedEntries = walletRentTotals.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); + final sortedEntries = walletRentTotals.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); return Wrap( spacing: 8.0, @@ -305,10 +325,14 @@ class _RentDistributionByWalletChartState extends State result = {}; // Obtenir les données de loyers par wallet depuis le DataManager - Map> rentsByWallet = widget.dataManager.getRentsByWallet(); + Map> rentsByWallet = + widget.dataManager.getRentsByWallet(); // Calculer le total des loyers pour chaque wallet for (var walletEntry in rentsByWallet.entries) { @@ -360,7 +389,8 @@ class _RentDistributionByWalletChartState extends State tokenRents = walletEntry.value; // Somme des loyers pour tous les tokens de ce wallet - double totalForWallet = tokenRents.values.fold(0.0, (sum, rent) => sum + rent); + double totalForWallet = + tokenRents.values.fold(0.0, (sum, rent) => sum + rent); result[wallet] = totalForWallet; } diff --git a/lib/pages/Statistics/wallet/charts/rent_graph.dart b/lib/pages/Statistics/wallet/charts/rent_graph.dart index 9e2edcb..90edecb 100644 --- a/lib/pages/Statistics/wallet/charts/rent_graph.dart +++ b/lib/pages/Statistics/wallet/charts/rent_graph.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:intl/intl.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/components/charts/generic_chart_widget.dart'; @@ -69,16 +68,17 @@ class _RentGraphState extends State { final dataManager = widget.dataManager; if (dataManager.rentData.isNotEmpty) { double cumulativeRent = 0; - + // Trier les données par date List> sortedData = List.from(dataManager.rentData) - ..sort((a, b) => DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); - + ..sort((a, b) => + DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); + for (var entry in sortedData) { double rent = entry['rent']?.toDouble() ?? 0.0; DateTime date = DateTime.parse(entry['date']); cumulativeRent += rent; - + rentRecords.add(RentRecord( timestamp: date, rent: currencyUtils.convert(rent), @@ -88,7 +88,7 @@ class _RentGraphState extends State { } } else { double cumulativeRent = 0; - + // Trier les données par date List> sortedData = List.from(groupedData) ..sort((a, b) { @@ -97,12 +97,12 @@ class _RentGraphState extends State { DateTime dateB = _parseDate(b['date']); return dateA.compareTo(dateB); }); - + for (var entry in sortedData) { double rent = entry['rent']?.toDouble() ?? 0.0; DateTime date = _parseDate(entry['date']); cumulativeRent += rent; - + rentRecords.add(RentRecord( timestamp: date, rent: currencyUtils.convert(rent), @@ -110,11 +110,12 @@ class _RentGraphState extends State { )); } } - - debugPrint("🔄 Conversion des données de loyer: ${rentRecords.length} enregistrements"); + + debugPrint( + "🔄 Conversion des données de loyer: ${rentRecords.length} enregistrements"); return rentRecords; } - + // Helper pour analyser différents formats de date DateTime _parseDate(String dateStr) { try { @@ -123,19 +124,20 @@ class _RentGraphState extends State { // Format yyyy/MM/dd List parts = dateStr.split('/'); if (parts.length == 3) { - return DateTime(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); + return DateTime( + int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); } // Format MM/yyyy if (parts.length == 2) { return DateTime(int.parse(parts[1]), int.parse(parts[0]), 1); } } - + // Format année seule if (dateStr.length == 4 && RegExp(r'^\d{4}$').hasMatch(dateStr)) { return DateTime(int.parse(dateStr), 1, 1); } - + // Format ISO return DateTime.parse(dateStr); } catch (e) { @@ -147,13 +149,14 @@ class _RentGraphState extends State { @override Widget build(BuildContext context) { final currencyUtils = Provider.of(context, listen: false); - + // Convertir les données au format requis par GenericChartWidget List rentRecords = _convertRentData(); - + // Utiliser GenericChartWidget avec le switch intégré return GenericChartWidget( - title: "Loyer", // Titre de base (peut être remplacé par les labels cumulatifs) + title: + "Loyer", // Titre de base (peut être remplacé par les labels cumulatifs) chartColor: const Color(0xFF007AFF), dataList: rentRecords, selectedPeriod: widget.selectedPeriod, @@ -164,7 +167,8 @@ class _RentGraphState extends State { onTimeRangeChanged: widget.onTimeRangeChanged, timeOffset: widget.timeOffset, onTimeOffsetChanged: widget.onTimeOffsetChanged, - getYValue: (record) => _showCumulativeRent ? record.cumulativeRent : record.rent, + getYValue: (record) => + _showCumulativeRent ? record.cumulativeRent : record.rent, getTimestamp: (record) => record.timestamp, valuePrefix: currencyUtils.currencySymbol, // Nouveaux paramètres pour le switch diff --git a/lib/pages/Statistics/wallet/charts/roi_graph.dart b/lib/pages/Statistics/wallet/charts/roi_graph.dart index 288f648..a66a984 100644 --- a/lib/pages/Statistics/wallet/charts/roi_graph.dart +++ b/lib/pages/Statistics/wallet/charts/roi_graph.dart @@ -59,39 +59,40 @@ class _RoiHistoryGraphState extends State { return EditableROIRecord( record, TextEditingController(text: record.roi.toStringAsFixed(2)), - TextEditingController(text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), + TextEditingController( + text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), ); }).toList(); } - void _updateROIValue(DataManager dataManager, EditableROIRecord editableRecord, double newValue) { + void _updateROIValue(DataManager dataManager, + EditableROIRecord editableRecord, double newValue) { // Récupérer l'index de l'enregistrement original - final index = dataManager.roiHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.roi == editableRecord.original.roi - ); - + final index = dataManager.roiHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.roi == editableRecord.original.roi); + if (index != -1) { // Créer un nouvel enregistrement avec la valeur mise à jour final updatedRecord = ROIRecord( timestamp: editableRecord.original.timestamp, roi: newValue, ); - + // Mettre à jour la liste dataManager.roiHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveRoiHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour l'enregistrement éditable editableRecord.valueController.text = newValue.toStringAsFixed(2); - + // Pour le débogage print('ROI mis à jour à l\'index $index: $newValue'); } else { @@ -99,66 +100,68 @@ class _RoiHistoryGraphState extends State { } } - void _updateROIDate(DataManager dataManager, EditableROIRecord editableRecord, DateTime newDate) { + void _updateROIDate(DataManager dataManager, EditableROIRecord editableRecord, + DateTime newDate) { // Récupérer l'index de l'enregistrement original - final index = dataManager.roiHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.roi == editableRecord.original.roi - ); - + final index = dataManager.roiHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.roi == editableRecord.original.roi); + if (index != -1) { // Créer un nouvel enregistrement avec la date mise à jour final updatedRecord = ROIRecord( timestamp: newDate, roi: editableRecord.original.roi, ); - + // Mettre à jour la liste dataManager.roiHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveRoiHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour l'enregistrement éditable - editableRecord.dateController.text = DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); - + editableRecord.dateController.text = + DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); + // Pour le débogage - print('Date ROI mise à jour à l\'index $index: ${newDate.toIso8601String()}'); + print( + 'Date ROI mise à jour à l\'index $index: ${newDate.toIso8601String()}'); } else { print('Enregistrement non trouvé pour la mise à jour de la date'); } } - void _deleteROIRecord(DataManager dataManager, EditableROIRecord editableRecord, StateSetter setState) { + void _deleteROIRecord(DataManager dataManager, + EditableROIRecord editableRecord, StateSetter setState) { // Récupérer l'index de l'enregistrement original - final index = dataManager.roiHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.roi == editableRecord.original.roi - ); - + final index = dataManager.roiHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.roi == editableRecord.original.roi); + if (index != -1) { // Supprimer de la liste dataManager.roiHistory.removeAt(index); - + // Sauvegarder dans Hive dataManager.saveRoiHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Pour le débogage print('ROI supprimé à l\'index $index'); - + // Recréer les enregistrements éditables setState(() { _editableRecords = _createEditableRecords( - List.from(dataManager.roiHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(dataManager.roiHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); }); } else { print('Enregistrement non trouvé pour la suppression'); @@ -168,8 +171,8 @@ class _RoiHistoryGraphState extends State { void _showEditModal(BuildContext context, DataManager dataManager) { // Créer des enregistrements éditables à partir des enregistrements triés _editableRecords = _createEditableRecords( - List.from(dataManager.roiHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(dataManager.roiHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); showModalBottomSheet( context: context, @@ -191,7 +194,7 @@ class _RoiHistoryGraphState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -1), ), @@ -207,7 +210,9 @@ class _RoiHistoryGraphState extends State { Text( 'Éditer l\'historique ROI', style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -226,7 +231,10 @@ class _RoiHistoryGraphState extends State { child: Text( "Aucun historique disponible", style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -245,7 +253,10 @@ class _RoiHistoryGraphState extends State { "Date", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -257,7 +268,10 @@ class _RoiHistoryGraphState extends State { "ROI", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -269,7 +283,10 @@ class _RoiHistoryGraphState extends State { "Actions", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), textAlign: TextAlign.right, ), @@ -283,59 +300,87 @@ class _RoiHistoryGraphState extends State { SizedBox( width: 150, child: TextField( - controller: editableRecord.dateController, - keyboardType: TextInputType.datetime, - textInputAction: TextInputAction.done, + controller: + editableRecord.dateController, + keyboardType: + TextInputType.datetime, + textInputAction: + TextInputAction.done, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(value); - _updateROIDate(dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(value); + _updateROIDate(dataManager, + editableRecord, newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, onEditingComplete: () { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(editableRecord.dateController.text); - _updateROIDate(dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(editableRecord + .dateController.text); + _updateROIDate(dataManager, + editableRecord, newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, @@ -346,62 +391,90 @@ class _RoiHistoryGraphState extends State { SizedBox( width: 100, child: TextField( - controller: editableRecord.valueController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - textInputAction: TextInputAction.done, + controller: + editableRecord.valueController, + keyboardType: const TextInputType + .numberWithOptions( + decimal: true), + textInputAction: + TextInputAction.done, inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d*')), ], style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { - double? newValue = double.tryParse(value); + double? newValue = + double.tryParse(value); if (newValue != null) { - _updateROIValue(dataManager, editableRecord, newValue); - FocusScope.of(context).unfocus(); + _updateROIValue(dataManager, + editableRecord, newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: $value'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: $value'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, onEditingComplete: () { - double? newValue = double.tryParse(editableRecord.valueController.text); + double? newValue = + double.tryParse(editableRecord + .valueController.text); if (newValue != null) { - _updateROIValue(dataManager, editableRecord, newValue); - FocusScope.of(context).unfocus(); + _updateROIValue(dataManager, + editableRecord, newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: ${editableRecord.valueController.text}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: ${editableRecord.valueController.text}'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, @@ -412,7 +485,8 @@ class _RoiHistoryGraphState extends State { SizedBox( width: 60, child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ IconButton( icon: Icon( @@ -421,7 +495,8 @@ class _RoiHistoryGraphState extends State { size: 20, ), onPressed: () { - _deleteROIRecord(dataManager, editableRecord, setState); + _deleteROIRecord(dataManager, + editableRecord, setState); }, ), ], @@ -445,46 +520,54 @@ class _RoiHistoryGraphState extends State { for (var editableRecord in _editableRecords) { try { // Mettre à jour la date - final dateText = editableRecord.dateController.text; - final DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(dateText); - + final dateText = + editableRecord.dateController.text; + final DateTime newDate = + DateFormat('yyyy-MM-dd HH:mm:ss') + .parse(dateText); + // Mettre à jour la valeur - final valueText = editableRecord.valueController.text; - final double? newValue = double.tryParse(valueText); - + final valueText = + editableRecord.valueController.text; + final double? newValue = + double.tryParse(valueText); + if (newValue != null) { // Trouver l'index dans la liste originale - final index = dataManager.roiHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.roi == editableRecord.original.roi - ); - + final index = dataManager.roiHistory.indexWhere( + (r) => + r.timestamp.isAtSameMomentAs( + editableRecord + .original.timestamp) && + r.roi == editableRecord.original.roi); + if (index != -1) { // Créer un nouvel enregistrement avec les nouvelles valeurs final updatedRecord = ROIRecord( timestamp: newDate, roi: newValue, ); - + // Mettre à jour la liste dataManager.roiHistory[index] = updatedRecord; - print('Mise à jour index $index: ${newDate.toIso8601String()} -> $newValue'); + print( + 'Mise à jour index $index: ${newDate.toIso8601String()} -> $newValue'); } } } catch (e) { print('Erreur lors de la mise à jour: $e'); } } - + // Sauvegarder dans Hive dataManager.saveRoiHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Fermer le modal Navigator.pop(context); - + // Forcer la mise à jour du widget parent setState(() {}); }, @@ -500,7 +583,9 @@ class _RoiHistoryGraphState extends State { child: Text( 'Sauvegarder', style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), diff --git a/lib/pages/Statistics/wallet/charts/token_distribution_by_product_type_chart.dart b/lib/pages/Statistics/wallet/charts/token_distribution_by_product_type_chart.dart index 0e9c7c7..2209bf0 100644 --- a/lib/pages/Statistics/wallet/charts/token_distribution_by_product_type_chart.dart +++ b/lib/pages/Statistics/wallet/charts/token_distribution_by_product_type_chart.dart @@ -15,10 +15,12 @@ class TokenDistributionByProductTypeChart extends StatefulWidget { }); @override - State createState() => _TokenDistributionByProductTypeChartState(); + State createState() => + _TokenDistributionByProductTypeChartState(); } -class _TokenDistributionByProductTypeChartState extends State { +class _TokenDistributionByProductTypeChartState + extends State { final ValueNotifier _selectedIndexNotifier = ValueNotifier(null); @override @@ -36,7 +38,7 @@ class _TokenDistributionByProductTypeChartState extends State= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterText(selectedIndex), @@ -112,7 +119,7 @@ class _TokenDistributionByProductTypeChartState extends State _buildDonutChartData(int? selectedIndex) { final appState = Provider.of(context); final tokensByType = _calculateTokensByProductType(); - + if (tokensByType.isEmpty) return []; final int total = tokensByType.values.fold(0, (sum, value) => sum + value); @@ -133,15 +140,17 @@ class _TokenDistributionByProductTypeChartState extends State(context); final tokensByType = _calculateTokensByProductType(); - + if (tokensByType.isEmpty) return Container(); final int total = tokensByType.values.fold(0, (sum, value) => sum + value); @@ -259,16 +268,21 @@ class _TokenDistributionByProductTypeChartState extends State _calculateTokensByProductType() { Map tokensByProductType = {}; - + for (var token in widget.dataManager.portfolio) { final String productType = token['productType'] ?? 'other'; - tokensByProductType[productType] = (tokensByProductType[productType] ?? 0) + 1; + tokensByProductType[productType] = + (tokensByProductType[productType] ?? 0) + 1; } - + return tokensByProductType; } - String _getLocalizedProductTypeName(BuildContext context, String productType) { + String _getLocalizedProductTypeName( + BuildContext context, String productType) { switch (productType.toLowerCase()) { case 'real_estate_rental': return S.of(context).productTypeRealEstateRental; @@ -333,4 +353,4 @@ class _TokenDistributionByProductTypeChartState extends State _TransactionAnalysisChartState(); + _TransactionAnalysisChartState createState() => + _TransactionAnalysisChartState(); } class _TransactionAnalysisChartState extends State { @@ -34,7 +35,7 @@ class _TransactionAnalysisChartState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -44,7 +45,7 @@ class _TransactionAnalysisChartState extends State { end: Alignment.bottomRight, colors: [ Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), + Theme.of(context).cardColor.withValues(alpha: 0.8), ], ), ), @@ -66,8 +67,12 @@ class _TransactionAnalysisChartState extends State { DropdownButton( value: _analysisType, items: [ - DropdownMenuItem(value: 'count', child: Text(S.of(context).transactionCount)), - DropdownMenuItem(value: 'volume', child: Text(S.of(context).transactionVolume)), + DropdownMenuItem( + value: 'count', + child: Text(S.of(context).transactionCount)), + DropdownMenuItem( + value: 'volume', + child: Text(S.of(context).transactionVolume)), ], onChanged: (String? value) { setState(() { @@ -89,22 +94,28 @@ class _TransactionAnalysisChartState extends State { children: [ PieChart( PieChartData( - sections: _buildTransactionDonutChartData(selectedIndex), + sections: + _buildTransactionDonutChartData(selectedIndex), centerSpaceRadius: 65, sectionsSpace: 3, borderData: FlBorderData(show: false), pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, PieTouchResponse? response) { - if (response != null && response.touchedSection != null) { - final touchedIndex = response.touchedSection!.touchedSectionIndex; - _selectedIndexNotifier.value = touchedIndex >= 0 ? touchedIndex : null; + touchCallback: (FlTouchEvent event, + PieTouchResponse? response) { + if (response != null && + response.touchedSection != null) { + final touchedIndex = response + .touchedSection!.touchedSectionIndex; + _selectedIndexNotifier.value = + touchedIndex >= 0 ? touchedIndex : null; } else { _selectedIndexNotifier.value = null; } }, ), ), - swapAnimationDuration: const Duration(milliseconds: 300), + swapAnimationDuration: + const Duration(milliseconds: 300), swapAnimationCurve: Curves.easeInOutCubic, ), _buildCenterText(selectedIndex), @@ -131,7 +142,7 @@ class _TransactionAnalysisChartState extends State { 'transfer': 0, 'yam': 0, }; - + Map transactionVolumes = { 'purchase': 0.0, 'transfer': 0.0, @@ -139,16 +150,20 @@ class _TransactionAnalysisChartState extends State { }; // Parcourir toutes les transactions par token - for (var tokenTransactions in widget.dataManager.transactionsByToken.values) { + for (var tokenTransactions + in widget.dataManager.transactionsByToken.values) { for (var transaction in tokenTransactions) { - final String transactionType = transaction['transactionType'] ?? 'unknown'; + final String transactionType = + transaction['transactionType'] ?? 'unknown'; final double amount = (transaction['amount'] ?? 0.0).toDouble(); final double price = (transaction['price'] ?? 0.0).toDouble(); final double volume = amount * price; if (transactionCounts.containsKey(transactionType)) { - transactionCounts[transactionType] = transactionCounts[transactionType]! + 1; - transactionVolumes[transactionType] = transactionVolumes[transactionType]! + volume; + transactionCounts[transactionType] = + transactionCounts[transactionType]! + 1; + transactionVolumes[transactionType] = + transactionVolumes[transactionType]! + volume; } } } @@ -159,27 +174,29 @@ class _TransactionAnalysisChartState extends State { }; } - List _buildTransactionDonutChartData(int? selectedIndex) { + List _buildTransactionDonutChartData( + int? selectedIndex) { final Map transactionData = _calculateTransactionData(); - final Map dataToUse = _analysisType == 'count' - ? transactionData['counts'] + final Map dataToUse = _analysisType == 'count' + ? transactionData['counts'] : transactionData['volumes']; - - final double total = dataToUse.values.fold(0.0, (sum, value) => sum + value); + + final double total = + dataToUse.values.fold(0.0, (sum, value) => sum + value); if (total <= 0) { return [ PieChartSectionData( value: 1, title: '', - color: Colors.grey.withOpacity(0.2), + color: Colors.grey.withValues(alpha: 0.2), radius: 40, ) ]; } - final List> sortedEntries = dataToUse.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + dataToUse.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); return sortedEntries.asMap().entries.map((entry) { final index = entry.key; @@ -208,15 +225,17 @@ class _TransactionAnalysisChartState extends State { return PieChartSectionData( value: value.toDouble(), title: percentage < 5 ? '' : '${percentage.toStringAsFixed(1)}%', - color: color.withOpacity(opacity), + color: color.withValues(alpha: opacity), radius: isSelected ? 52 : 45, titleStyle: TextStyle( - fontSize: isSelected ? 14 + Provider.of(context).getTextSizeOffset() : 10 + Provider.of(context).getTextSizeOffset(), + fontSize: isSelected + ? 14 + Provider.of(context).getTextSizeOffset() + : 10 + Provider.of(context).getTextSizeOffset(), color: Colors.white, fontWeight: FontWeight.w600, shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(1, 1), ), @@ -241,7 +260,7 @@ class _TransactionAnalysisChartState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: const Offset(0, 1), ), @@ -252,11 +271,12 @@ class _TransactionAnalysisChartState extends State { Widget _buildCenterText(int? selectedIndex) { final Map transactionData = _calculateTransactionData(); - final Map dataToUse = _analysisType == 'count' - ? transactionData['counts'] + final Map dataToUse = _analysisType == 'count' + ? transactionData['counts'] : transactionData['volumes']; - - final double total = dataToUse.values.fold(0.0, (sum, value) => sum + value); + + final double total = + dataToUse.values.fold(0.0, (sum, value) => sum + value); final currencyUtils = Provider.of(context, listen: false); if (selectedIndex == null) { @@ -273,9 +293,10 @@ class _TransactionAnalysisChartState extends State { ), const SizedBox(height: 4), Text( - _analysisType == 'count' + _analysisType == 'count' ? total.toStringAsFixed(0) - : currencyUtils.getFormattedAmount(currencyUtils.convert(total), currencyUtils.currencySymbol, true), + : currencyUtils.getFormattedAmount(currencyUtils.convert(total), + currencyUtils.currencySymbol, true), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -286,13 +307,14 @@ class _TransactionAnalysisChartState extends State { ); } - final List> sortedEntries = dataToUse.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + dataToUse.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); if (selectedIndex >= sortedEntries.length) return Container(); final selectedEntry = sortedEntries[selectedIndex]; - final String transactionTypeName = _getTransactionTypeName(selectedEntry.key); + final String transactionTypeName = + _getTransactionTypeName(selectedEntry.key); return Column( mainAxisSize: MainAxisSize.min, @@ -308,9 +330,12 @@ class _TransactionAnalysisChartState extends State { ), const SizedBox(height: 4), Text( - _analysisType == 'count' + _analysisType == 'count' ? selectedEntry.value.toStringAsFixed(0) - : currencyUtils.getFormattedAmount(currencyUtils.convert(selectedEntry.value), currencyUtils.currencySymbol, true), + : currencyUtils.getFormattedAmount( + currencyUtils.convert(selectedEntry.value), + currencyUtils.currencySymbol, + true), style: TextStyle( fontSize: 14 + Provider.of(context).getTextSizeOffset(), color: Colors.grey.shade600, @@ -349,15 +374,15 @@ class _TransactionAnalysisChartState extends State { Widget _buildTransactionLegend() { final Map transactionData = _calculateTransactionData(); - final Map dataToUse = _analysisType == 'count' - ? transactionData['counts'] + final Map dataToUse = _analysisType == 'count' + ? transactionData['counts'] : transactionData['volumes']; - + final appState = Provider.of(context); final currencyUtils = Provider.of(context, listen: false); - final List> sortedEntries = dataToUse.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); + final List> sortedEntries = + dataToUse.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); return Wrap( spacing: 8.0, @@ -372,16 +397,21 @@ class _TransactionAnalysisChartState extends State { return InkWell( onTap: () { - _selectedIndexNotifier.value = (_selectedIndexNotifier.value == index) ? null : index; + _selectedIndexNotifier.value = + (_selectedIndexNotifier.value == index) ? null : index; }, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), decoration: BoxDecoration( - color: _selectedIndexNotifier.value == index ? color.withOpacity(0.1) : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color.withValues(alpha: 0.1) + : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( - color: _selectedIndexNotifier.value == index ? color : Colors.transparent, + color: _selectedIndexNotifier.value == index + ? color + : Colors.transparent, width: 1, ), ), @@ -396,7 +426,7 @@ class _TransactionAnalysisChartState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1), ), @@ -408,8 +438,12 @@ class _TransactionAnalysisChartState extends State { '$typeName: ${_analysisType == 'count' ? value.toStringAsFixed(0) : currencyUtils.getFormattedAmount(currencyUtils.convert(value), currencyUtils.currencySymbol, true)}', style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: _selectedIndexNotifier.value == index ? color : Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: _selectedIndexNotifier.value == index ? FontWeight.w600 : FontWeight.normal, + color: _selectedIndexNotifier.value == index + ? color + : Theme.of(context).textTheme.bodyMedium?.color, + fontWeight: _selectedIndexNotifier.value == index + ? FontWeight.w600 + : FontWeight.normal, ), ), ], @@ -419,4 +453,4 @@ class _TransactionAnalysisChartState extends State { }).toList(), ); } -} \ No newline at end of file +} diff --git a/lib/pages/Statistics/wallet/charts/wallet_balance_graph.dart b/lib/pages/Statistics/wallet/charts/wallet_balance_graph.dart index f511c96..d4a3180 100644 --- a/lib/pages/Statistics/wallet/charts/wallet_balance_graph.dart +++ b/lib/pages/Statistics/wallet/charts/wallet_balance_graph.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; @@ -14,7 +13,8 @@ class EditableBalanceRecord { final TextEditingController valueController; final TextEditingController dateController; - EditableBalanceRecord(this.original, this.valueController, this.dateController); + EditableBalanceRecord( + this.original, this.valueController, this.dateController); } class WalletBalanceGraph extends StatefulWidget { @@ -55,23 +55,25 @@ class _WalletBalanceGraphState extends State { super.dispose(); } - List _createEditableRecords(List records) { + List _createEditableRecords( + List records) { return records.map((record) { return EditableBalanceRecord( record, TextEditingController(text: record.balance.toStringAsFixed(2)), - TextEditingController(text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), + TextEditingController( + text: DateFormat('yyyy-MM-dd HH:mm:ss').format(record.timestamp)), ); }).toList(); } - void _updateBalanceValue(DataManager dataManager, EditableBalanceRecord editableRecord, double newValue) { + void _updateBalanceValue(DataManager dataManager, + EditableBalanceRecord editableRecord, double newValue) { // Récupérer l'index de l'enregistrement original - final index = dataManager.walletBalanceHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.balance == editableRecord.original.balance - ); - + final index = dataManager.walletBalanceHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.balance == editableRecord.original.balance); + if (index != -1) { // Créer un nouvel enregistrement avec la valeur mise à jour final updatedRecord = BalanceRecord( @@ -79,21 +81,21 @@ class _WalletBalanceGraphState extends State { balance: newValue, tokenType: editableRecord.original.tokenType, ); - + // Mettre à jour la liste dataManager.walletBalanceHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveWalletBalanceHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour l'enregistrement éditable editableRecord.valueController.text = newValue.toStringAsFixed(2); - + // Pour le débogage print('Balance mise à jour à l\'index $index: $newValue'); } else { @@ -101,13 +103,13 @@ class _WalletBalanceGraphState extends State { } } - void _updateBalanceDate(DataManager dataManager, EditableBalanceRecord editableRecord, DateTime newDate) { + void _updateBalanceDate(DataManager dataManager, + EditableBalanceRecord editableRecord, DateTime newDate) { // Récupérer l'index de l'enregistrement original - final index = dataManager.walletBalanceHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.balance == editableRecord.original.balance - ); - + final index = dataManager.walletBalanceHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.balance == editableRecord.original.balance); + if (index != -1) { // Créer un nouvel enregistrement avec la date mise à jour final updatedRecord = BalanceRecord( @@ -115,53 +117,55 @@ class _WalletBalanceGraphState extends State { balance: editableRecord.original.balance, tokenType: editableRecord.original.tokenType, ); - + // Mettre à jour la liste dataManager.walletBalanceHistory[index] = updatedRecord; // Mettre à jour l'original dans l'enregistrement éditable editableRecord.original = updatedRecord; - + // Sauvegarder dans Hive dataManager.saveWalletBalanceHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Mettre à jour l'enregistrement éditable - editableRecord.dateController.text = DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); - + editableRecord.dateController.text = + DateFormat('yyyy-MM-dd HH:mm:ss').format(newDate); + // Pour le débogage - print('Date balance mise à jour à l\'index $index: ${newDate.toIso8601String()}'); + print( + 'Date balance mise à jour à l\'index $index: ${newDate.toIso8601String()}'); } else { print('Enregistrement non trouvé pour la mise à jour de la date'); } } - void _deleteBalanceRecord(DataManager dataManager, EditableBalanceRecord editableRecord, StateSetter setState) { + void _deleteBalanceRecord(DataManager dataManager, + EditableBalanceRecord editableRecord, StateSetter setState) { // Récupérer l'index de l'enregistrement original - final index = dataManager.walletBalanceHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.balance == editableRecord.original.balance - ); - + final index = dataManager.walletBalanceHistory.indexWhere((r) => + r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && + r.balance == editableRecord.original.balance); + if (index != -1) { // Supprimer de la liste dataManager.walletBalanceHistory.removeAt(index); - + // Sauvegarder dans Hive dataManager.saveWalletBalanceHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Pour le débogage print('Balance supprimée à l\'index $index'); - + // Recréer les enregistrements éditables setState(() { _editableRecords = _createEditableRecords( - List.from(dataManager.walletBalanceHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(dataManager.walletBalanceHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); }); } else { print('Enregistrement non trouvé pour la suppression'); @@ -171,8 +175,8 @@ class _WalletBalanceGraphState extends State { void _showEditModal(BuildContext context, DataManager dataManager) { // Créer des enregistrements éditables à partir des enregistrements triés _editableRecords = _createEditableRecords( - List.from(dataManager.walletBalanceHistory)..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ); + List.from(dataManager.walletBalanceHistory) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp))); showModalBottomSheet( context: context, @@ -194,7 +198,7 @@ class _WalletBalanceGraphState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -1), ), @@ -210,7 +214,9 @@ class _WalletBalanceGraphState extends State { Text( S.of(context).editWalletBalance, style: TextStyle( - fontSize: 20 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 20 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -229,7 +235,9 @@ class _WalletBalanceGraphState extends State { child: Text( "Aucun historique disponible", style: TextStyle( - fontSize: 16 + Provider.of(context).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context) + .getTextSizeOffset(), color: Colors.grey.shade600, ), ), @@ -248,7 +256,9 @@ class _WalletBalanceGraphState extends State { S.of(context).date, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), ), ), ), @@ -260,7 +270,9 @@ class _WalletBalanceGraphState extends State { S.of(context).balance, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), ), ), ), @@ -272,7 +284,9 @@ class _WalletBalanceGraphState extends State { "Actions", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), ), textAlign: TextAlign.right, ), @@ -286,59 +300,86 @@ class _WalletBalanceGraphState extends State { SizedBox( width: 150, child: TextField( - controller: editableRecord.dateController, - keyboardType: TextInputType.datetime, - textInputAction: TextInputAction.done, + controller: + editableRecord.dateController, + keyboardType: + TextInputType.datetime, + textInputAction: + TextInputAction.done, style: TextStyle( - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(value); - _updateBalanceDate(dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(value); + _updateBalanceDate(dataManager, + editableRecord, newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, onEditingComplete: () { try { - DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(editableRecord.dateController.text); - _updateBalanceDate(dataManager, editableRecord, newDate); - FocusScope.of(context).unfocus(); + DateTime newDate = DateFormat( + 'yyyy-MM-dd HH:mm:ss') + .parse(editableRecord + .dateController.text); + _updateBalanceDate(dataManager, + editableRecord, newDate); + FocusScope.of(context) + .unfocus(); } catch (e) { - print('Erreur de format de date: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Format de date invalide')), + print( + 'Erreur de format de date: $e'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Format de date invalide')), ); } }, @@ -349,62 +390,89 @@ class _WalletBalanceGraphState extends State { SizedBox( width: 100, child: TextField( - controller: editableRecord.valueController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - textInputAction: TextInputAction.done, + controller: + editableRecord.valueController, + keyboardType: const TextInputType + .numberWithOptions( + decimal: true), + textInputAction: + TextInputAction.done, inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d*')), ], style: TextStyle( - fontSize: 14 + Provider.of(context).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), ), decoration: InputDecoration( filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( color: Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8), borderSide: BorderSide( - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), - contentPadding: const EdgeInsets.symmetric( + contentPadding: + const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { - double? newValue = double.tryParse(value); + double? newValue = + double.tryParse(value); if (newValue != null) { - _updateBalanceValue(dataManager, editableRecord, newValue); - FocusScope.of(context).unfocus(); + _updateBalanceValue(dataManager, + editableRecord, newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: $value'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: $value'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, onEditingComplete: () { - double? newValue = double.tryParse(editableRecord.valueController.text); + double? newValue = + double.tryParse(editableRecord + .valueController.text); if (newValue != null) { - _updateBalanceValue(dataManager, editableRecord, newValue); - FocusScope.of(context).unfocus(); + _updateBalanceValue(dataManager, + editableRecord, newValue); + FocusScope.of(context) + .unfocus(); } else { - print('Valeur non valide: ${editableRecord.valueController.text}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Valeur non valide')), + print( + 'Valeur non valide: ${editableRecord.valueController.text}'); + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Valeur non valide')), ); } }, @@ -415,16 +483,23 @@ class _WalletBalanceGraphState extends State { SizedBox( width: 60, child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ IconButton( icon: Icon( Icons.delete_outline, color: Colors.red.shade700, - size: 20 + Provider.of(context).getTextSizeOffset(), + size: 20 + + Provider.of( + context) + .getTextSizeOffset(), ), onPressed: () { - _deleteBalanceRecord(dataManager, editableRecord, setState); + _deleteBalanceRecord( + dataManager, + editableRecord, + setState); }, ), ], @@ -448,47 +523,58 @@ class _WalletBalanceGraphState extends State { for (var editableRecord in _editableRecords) { try { // Mettre à jour la date - final dateText = editableRecord.dateController.text; - final DateTime newDate = DateFormat('yyyy-MM-dd HH:mm:ss').parse(dateText); - + final dateText = + editableRecord.dateController.text; + final DateTime newDate = + DateFormat('yyyy-MM-dd HH:mm:ss') + .parse(dateText); + // Mettre à jour la valeur - final valueText = editableRecord.valueController.text; - final double? newValue = double.tryParse(valueText); - + final valueText = + editableRecord.valueController.text; + final double? newValue = + double.tryParse(valueText); + if (newValue != null) { // Trouver l'index dans la liste originale - final index = dataManager.walletBalanceHistory.indexWhere((r) => - r.timestamp.isAtSameMomentAs(editableRecord.original.timestamp) && - r.balance == editableRecord.original.balance - ); - + final index = dataManager.walletBalanceHistory + .indexWhere((r) => + r.timestamp.isAtSameMomentAs( + editableRecord + .original.timestamp) && + r.balance == + editableRecord.original.balance); + if (index != -1) { // Créer un nouvel enregistrement avec les nouvelles valeurs final updatedRecord = BalanceRecord( timestamp: newDate, balance: newValue, - tokenType: editableRecord.original.tokenType, + tokenType: + editableRecord.original.tokenType, ); - + // Mettre à jour la liste - dataManager.walletBalanceHistory[index] = updatedRecord; - print('Mise à jour index $index: ${newDate.toIso8601String()} -> $newValue'); + dataManager.walletBalanceHistory[index] = + updatedRecord; + print( + 'Mise à jour index $index: ${newDate.toIso8601String()} -> $newValue'); } } } catch (e) { print('Erreur lors de la mise à jour: $e'); } } - + // Sauvegarder dans Hive dataManager.saveWalletBalanceHistory(); - + // Notifier les écouteurs dataManager.notifyListeners(); - + // Fermer le modal Navigator.pop(context); - + // Forcer la mise à jour du widget parent setState(() {}); }, @@ -504,7 +590,9 @@ class _WalletBalanceGraphState extends State { child: Text( S.of(context).save, style: TextStyle( - fontSize: 16 + Provider.of(context).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), diff --git a/lib/pages/Statistics/wallet/wallet_stats.dart b/lib/pages/Statistics/wallet/wallet_stats.dart index 31b7254..2e3982d 100644 --- a/lib/pages/Statistics/wallet/wallet_stats.dart +++ b/lib/pages/Statistics/wallet/wallet_stats.dart @@ -50,17 +50,18 @@ class _WalletStats extends State { walletIsBarChart = prefs.getBool('walletIsBarChart') ?? false; roiIsBarChart = prefs.getBool('roiIsBarChart') ?? false; apyIsBarChart = prefs.getBool('apyIsBarChart') ?? true; - + // Charger les périodes sélectionnées - _selectedWalletPeriod = prefs.getString('walletPeriod') ?? S.of(context).week; + _selectedWalletPeriod = + prefs.getString('walletPeriod') ?? S.of(context).week; _selectedRoiPeriod = prefs.getString('roiPeriod') ?? S.of(context).week; _selectedApyPeriod = prefs.getString('apyPeriod') ?? S.of(context).week; - + // Charger les plages de temps _selectedWalletTimeRange = prefs.getString('walletTimeRange') ?? 'all'; _selectedRoiTimeRange = prefs.getString('roiTimeRange') ?? 'all'; _selectedApyTimeRange = prefs.getString('apyTimeRange') ?? 'all'; - + // Charger les offsets _walletTimeOffset = prefs.getInt('walletTimeOffset') ?? 0; _roiTimeOffset = prefs.getInt('roiTimeOffset') ?? 0; @@ -69,9 +70,10 @@ class _WalletStats extends State { try { final dataManager = Provider.of(context, listen: false); - + // Vérifier si les données sont déjà disponibles - if (!dataManager.isLoadingMain && dataManager.evmAddresses.isNotEmpty && + if (!dataManager.isLoadingMain && + dataManager.evmAddresses.isNotEmpty && dataManager.walletBalanceHistory.isNotEmpty) { debugPrint("📈 WalletStats: données déjà chargées, skip chargement"); } else { @@ -99,167 +101,171 @@ class _WalletStats extends State { final double fixedCardHeight = 380; return CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.only(top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isWideScreen ? 2 : 1, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - mainAxisExtent: fixedCardHeight, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - switch (index) { - case 0: - return TokenDistributionByWalletCard( - key: const ValueKey('token_distribution_by_wallet_card'), - dataManager: dataManager, - ); - case 1: - return RentDistributionByWalletChart( - key: const ValueKey('rent_distribution_by_wallet_chart'), - dataManager: dataManager, - ); - case 2: - return TokenDistributionByProductTypeChart( - key: const ValueKey('token_distribution_by_product_type_chart'), - dataManager: dataManager, - ); - case 3: - return WalletBalanceGraph( - key: const ValueKey('wallet_balance_graph'), - selectedPeriod: _selectedWalletPeriod, - onPeriodChanged: (period) { - setState(() { - _selectedWalletPeriod = period; - _savePeriodPreference('walletPeriod', period); - }); - }, - balanceIsBarChart: walletIsBarChart, - onChartTypeChanged: (isBarChart) { - setState(() { - walletIsBarChart = isBarChart; - _saveChartPreference('walletIsBarChart', walletIsBarChart); - }); - }, - selectedTimeRange: _selectedWalletTimeRange, - onTimeRangeChanged: (newRange) { - setState(() { - _selectedWalletTimeRange = newRange; - _saveTimeRangePreference('walletTimeRange', newRange); - _walletTimeOffset = 0; - _saveTimeOffsetPreference('walletTimeOffset', 0); - }); - }, - timeOffset: _walletTimeOffset, - onTimeOffsetChanged: (newOffset) { - setState(() { - _walletTimeOffset = newOffset; - _saveTimeOffsetPreference('walletTimeOffset', newOffset); - }); - }, - ); - case 4: - return RoiHistoryGraph( - key: const ValueKey('roi_graph'), - selectedPeriod: _selectedRoiPeriod, - onPeriodChanged: (period) { - setState(() { - _selectedRoiPeriod = period; - _savePeriodPreference('roiPeriod', period); - }); - }, - roiIsBarChart: roiIsBarChart, - onChartTypeChanged: (isBarChart) { - setState(() { - roiIsBarChart = isBarChart; - _saveChartPreference('roiIsBarChart', roiIsBarChart); - }); - }, - selectedTimeRange: _selectedRoiTimeRange, - onTimeRangeChanged: (newRange) { - setState(() { - _selectedRoiTimeRange = newRange; - _saveTimeRangePreference('roiTimeRange', newRange); - _roiTimeOffset = 0; - _saveTimeOffsetPreference('roiTimeOffset', 0); - }); - }, - timeOffset: _roiTimeOffset, - onTimeOffsetChanged: (newOffset) { - setState(() { - _roiTimeOffset = newOffset; - _saveTimeOffsetPreference('roiTimeOffset', newOffset); - }); - }, - ); - case 5: - return ApyHistoryGraph( - key: const ValueKey('apy_graph'), - dataManager: dataManager, - selectedPeriod: _selectedApyPeriod, - onPeriodChanged: (period) { - setState(() { - _selectedApyPeriod = period; - _savePeriodPreference('apyPeriod', period); - }); - }, - apyIsBarChart: apyIsBarChart, - onChartTypeChanged: (isBarChart) { - setState(() { - apyIsBarChart = isBarChart; - _saveChartPreference('apyIsBarChart', apyIsBarChart); - }); - }, - selectedTimeRange: _selectedApyTimeRange, - onTimeRangeChanged: (newRange) { - setState(() { - _selectedApyTimeRange = newRange; - _saveTimeRangePreference('apyTimeRange', newRange); - _apyTimeOffset = 0; - _saveTimeOffsetPreference('apyTimeOffset', 0); - }); - }, - timeOffset: _apyTimeOffset, - onTimeOffsetChanged: (newOffset) { - setState(() { - _apyTimeOffset = newOffset; - _saveTimeOffsetPreference('apyTimeOffset', newOffset); - }); - }, - ); - case 6: - return TransactionAnalysisChart( - key: const ValueKey('transaction_analysis_chart'), - dataManager: dataManager, - ); - default: - return Container(); - } - }, - childCount: 7, - ), + slivers: [ + SliverPadding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 80.0, left: 8.0, right: 8.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isWideScreen ? 2 : 1, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + mainAxisExtent: fixedCardHeight, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + switch (index) { + case 0: + return TokenDistributionByWalletCard( + key: const ValueKey('token_distribution_by_wallet_card'), + dataManager: dataManager, + ); + case 1: + return RentDistributionByWalletChart( + key: const ValueKey('rent_distribution_by_wallet_chart'), + dataManager: dataManager, + ); + case 2: + return TokenDistributionByProductTypeChart( + key: const ValueKey( + 'token_distribution_by_product_type_chart'), + dataManager: dataManager, + ); + case 3: + return WalletBalanceGraph( + key: const ValueKey('wallet_balance_graph'), + selectedPeriod: _selectedWalletPeriod, + onPeriodChanged: (period) { + setState(() { + _selectedWalletPeriod = period; + _savePeriodPreference('walletPeriod', period); + }); + }, + balanceIsBarChart: walletIsBarChart, + onChartTypeChanged: (isBarChart) { + setState(() { + walletIsBarChart = isBarChart; + _saveChartPreference( + 'walletIsBarChart', walletIsBarChart); + }); + }, + selectedTimeRange: _selectedWalletTimeRange, + onTimeRangeChanged: (newRange) { + setState(() { + _selectedWalletTimeRange = newRange; + _saveTimeRangePreference('walletTimeRange', newRange); + _walletTimeOffset = 0; + _saveTimeOffsetPreference('walletTimeOffset', 0); + }); + }, + timeOffset: _walletTimeOffset, + onTimeOffsetChanged: (newOffset) { + setState(() { + _walletTimeOffset = newOffset; + _saveTimeOffsetPreference( + 'walletTimeOffset', newOffset); + }); + }, + ); + case 4: + return RoiHistoryGraph( + key: const ValueKey('roi_graph'), + selectedPeriod: _selectedRoiPeriod, + onPeriodChanged: (period) { + setState(() { + _selectedRoiPeriod = period; + _savePeriodPreference('roiPeriod', period); + }); + }, + roiIsBarChart: roiIsBarChart, + onChartTypeChanged: (isBarChart) { + setState(() { + roiIsBarChart = isBarChart; + _saveChartPreference('roiIsBarChart', roiIsBarChart); + }); + }, + selectedTimeRange: _selectedRoiTimeRange, + onTimeRangeChanged: (newRange) { + setState(() { + _selectedRoiTimeRange = newRange; + _saveTimeRangePreference('roiTimeRange', newRange); + _roiTimeOffset = 0; + _saveTimeOffsetPreference('roiTimeOffset', 0); + }); + }, + timeOffset: _roiTimeOffset, + onTimeOffsetChanged: (newOffset) { + setState(() { + _roiTimeOffset = newOffset; + _saveTimeOffsetPreference('roiTimeOffset', newOffset); + }); + }, + ); + case 5: + return ApyHistoryGraph( + key: const ValueKey('apy_graph'), + dataManager: dataManager, + selectedPeriod: _selectedApyPeriod, + onPeriodChanged: (period) { + setState(() { + _selectedApyPeriod = period; + _savePeriodPreference('apyPeriod', period); + }); + }, + apyIsBarChart: apyIsBarChart, + onChartTypeChanged: (isBarChart) { + setState(() { + apyIsBarChart = isBarChart; + _saveChartPreference('apyIsBarChart', apyIsBarChart); + }); + }, + selectedTimeRange: _selectedApyTimeRange, + onTimeRangeChanged: (newRange) { + setState(() { + _selectedApyTimeRange = newRange; + _saveTimeRangePreference('apyTimeRange', newRange); + _apyTimeOffset = 0; + _saveTimeOffsetPreference('apyTimeOffset', 0); + }); + }, + timeOffset: _apyTimeOffset, + onTimeOffsetChanged: (newOffset) { + setState(() { + _apyTimeOffset = newOffset; + _saveTimeOffsetPreference('apyTimeOffset', newOffset); + }); + }, + ); + case 6: + return TransactionAnalysisChart( + key: const ValueKey('transaction_analysis_chart'), + dataManager: dataManager, + ); + default: + return Container(); + } + }, + childCount: 7, ), ), - ], - ); + ), + ], + ); } // Méthodes pour sauvegarder les différentes préférences void _saveChartPreference(String key, bool value) { prefs.setBool(key, value); } - + void _savePeriodPreference(String key, String value) { prefs.setString(key, value); } - + void _saveTimeRangePreference(String key, String value) { prefs.setString(key, value); } - + void _saveTimeOffsetPreference(String key, int value) { prefs.setInt(key, value); } diff --git a/lib/pages/changelog_page.dart b/lib/pages/changelog_page.dart index 0e0c295..a036c30 100644 --- a/lib/pages/changelog_page.dart +++ b/lib/pages/changelog_page.dart @@ -20,7 +20,8 @@ class _ChangelogPageState extends State { } Future _fetchMarkdown() async { - const url = 'https://raw.githubusercontent.com/RealToken-Community/realtoken_apps/refs/heads/main/CHANGELOG.md'; + const url = + 'https://raw.githubusercontent.com/RealToken-Community/realtoken_apps/refs/heads/main/CHANGELOG.md'; try { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { @@ -30,7 +31,8 @@ class _ChangelogPageState extends State { }); } else { setState(() { - _markdownData = 'Erreur lors du chargement du contenu (code: ${response.statusCode})'; + _markdownData = + 'Erreur lors du chargement du contenu (code: ${response.statusCode})'; _isLoading = false; }); } diff --git a/lib/pages/dashboard/dashboard_page.dart b/lib/pages/dashboard/dashboard_page.dart index f20eb7d..e9e32e4 100644 --- a/lib/pages/dashboard/dashboard_page.dart +++ b/lib/pages/dashboard/dashboard_page.dart @@ -9,8 +9,6 @@ import 'package:realtoken_asset_tracker/utils/data_fetch_utils.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:flutter/rendering.dart'; import 'dart:ui'; // Pour ImageFilter import 'package:file_picker/file_picker.dart'; import 'package:archive/archive_io.dart'; @@ -18,11 +16,9 @@ import 'package:hive/hive.dart'; import 'dart:convert'; import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:realtoken_asset_tracker/models/balance_record.dart'; import 'widgets/portfolio_card.dart'; import 'widgets/rmm_card.dart'; -import 'widgets/properties_card.dart'; import 'widgets/real_estate_card.dart'; import 'widgets/loan_income_card.dart'; import 'widgets/factoring_card.dart'; @@ -46,24 +42,28 @@ class DashboardPageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { // Vérifier si les données sont déjà chargées final dataManager = Provider.of(context, listen: false); - + // Si les données principales sont déjà chargées (depuis main.dart) - if (!dataManager.isLoadingMain && dataManager.evmAddresses.isNotEmpty && dataManager.portfolio.isNotEmpty) { + if (!dataManager.isLoadingMain && + dataManager.evmAddresses.isNotEmpty && + dataManager.portfolio.isNotEmpty) { debugPrint("📊 Dashboard: données principales déjà chargées"); // Vérifier si les données de loyer sont aussi chargées if (dataManager.rentData.isNotEmpty) { - debugPrint("📊 Dashboard: données de loyer aussi chargées, skip chargement"); + debugPrint( + "📊 Dashboard: données de loyer aussi chargées, skip chargement"); setState(() { _isPageLoading = false; }); } else { - debugPrint("📊 Dashboard: données de loyer manquantes, chargement nécessaire"); + debugPrint( + "📊 Dashboard: données de loyer manquantes, chargement nécessaire"); await DataFetchUtils.loadDataWithCache(context); - setState(() { - _isPageLoading = false; - }); + setState(() { + _isPageLoading = false; + }); } - } + } // Sinon, charger les données avec cache else { debugPrint("📊 Dashboard: chargement des données nécessaire"); @@ -117,27 +117,31 @@ class DashboardPageState extends State { final prefs = await SharedPreferences.getInstance(); // Restaurer les préférences sauvegardées - List ethAddresses = List.from(preferencesData['ethAddresses'] ?? []); + List ethAddresses = + List.from(preferencesData['ethAddresses'] ?? []); String? userIdToAddresses = preferencesData['userIdToAddresses']; String? selectedCurrency = preferencesData['selectedCurrency']; - bool convertToSquareMeters = preferencesData['convertToSquareMeters'] ?? false; + bool convertToSquareMeters = + preferencesData['convertToSquareMeters'] ?? false; // Sauvegarder les préférences restaurées await prefs.setStringList('evmAddresses', ethAddresses); - if (userIdToAddresses != null) await prefs.setString('userIdToAddresses', userIdToAddresses); - if (selectedCurrency != null) await prefs.setString('selectedCurrency', selectedCurrency); + if (userIdToAddresses != null) + await prefs.setString('userIdToAddresses', userIdToAddresses); + if (selectedCurrency != null) + await prefs.setString('selectedCurrency', selectedCurrency); await prefs.setBool('convertToSquareMeters', convertToSquareMeters); } } - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(S.of(context).importSuccess)), ); - + // Rafraîchir les données après l'import await DataFetchUtils.refreshData(context); - + // Recharger la page pour refléter les changements setState(() { _isPageLoading = false; @@ -179,19 +183,23 @@ class DashboardPageState extends State { child: Text( S.of(context).noDataAvailable, style: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), ), ], ), - content: Text( - S.of(context).noWalletMessage, - style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), - ), - ), + content: Text( + S.of(context).noWalletMessage, + style: TextStyle( + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + ), + ), actions: [ Column( mainAxisSize: MainAxisSize.min, @@ -226,7 +234,8 @@ class DashboardPageState extends State { Navigator.of(context).pop(); Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const ManageEvmAddressesPage(), + builder: (context) => + const ManageEvmAddressesPage(), ), ); }, @@ -253,7 +262,8 @@ class DashboardPageState extends State { child: Text( 'Plus tard', style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, + color: + Theme.of(context).textTheme.bodyMedium?.color, ), ), ), @@ -277,7 +287,8 @@ class DashboardPageState extends State { } // Trier les données par date (la plus ancienne en premier) - rentData.sort((a, b) => DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); + rentData.sort((a, b) => + DateTime.parse(a['date']).compareTo(DateTime.parse(b['date']))); // Date du premier loyer final firstRentDate = DateTime.parse(rentData.first['date']); @@ -292,7 +303,9 @@ class DashboardPageState extends State { // Format plus lisible if (years > 0) { - return years == 1 ? "$years year ${months > 0 ? '$months month${months > 1 ? 's' : ''}' : ''}" : "$years years ${months > 0 ? '$months month${months > 1 ? 's' : ''}' : ''}"; + return years == 1 + ? "$years year ${months > 0 ? '$months month${months > 1 ? 's' : ''}' : ''}" + : "$years years ${months > 0 ? '$months month${months > 1 ? 's' : ''}' : ''}"; } else if (months > 0) { return "$months month${months > 1 ? 's' : ''}"; } else { @@ -305,37 +318,47 @@ class DashboardPageState extends State { try { final prefs = await SharedPreferences.getInstance(); final List wallets = prefs.getStringList('evmAddresses') ?? []; - + if (wallets.isEmpty) return true; // Pas de wallets = pas de problème - + final box = Hive.box('realTokens'); final DateTime now = DateTime.now(); - + // Calculer le début de la semaine actuelle (lundi) - final DateTime startOfCurrentWeek = now.subtract(Duration(days: now.weekday - 1)); - final DateTime startOfCurrentWeekMidnight = DateTime(startOfCurrentWeek.year, startOfCurrentWeek.month, startOfCurrentWeek.day); - - debugPrint('🔍 DEBUG ALERTE - Début de semaine: $startOfCurrentWeekMidnight'); + final DateTime startOfCurrentWeek = + now.subtract(Duration(days: now.weekday - 1)); + final DateTime startOfCurrentWeekMidnight = DateTime( + startOfCurrentWeek.year, + startOfCurrentWeek.month, + startOfCurrentWeek.day); + + debugPrint( + '🔍 DEBUG ALERTE - Début de semaine: $startOfCurrentWeekMidnight'); debugPrint('🔍 DEBUG ALERTE - Maintenant: $now'); - debugPrint('🔍 DEBUG ALERTE - ${wallets.length} wallets à vérifier: ${wallets.join(", ")}'); - + debugPrint( + '🔍 DEBUG ALERTE - ${wallets.length} wallets à vérifier: ${wallets.join(", ")}'); + // Vérifier le statut de chaque wallet pour les données basiques uniquement for (String wallet in wallets) { final lastSuccessTime = box.get('lastRentSuccess_$wallet'); if (lastSuccessTime != null) { final DateTime lastSuccess = DateTime.parse(lastSuccessTime); - final bool isAfterWeekStart = lastSuccess.isAfter(startOfCurrentWeekMidnight); - debugPrint('🔍 DEBUG ALERTE - Wallet $wallet basique: $lastSuccess (après début semaine: $isAfterWeekStart)'); + final bool isAfterWeekStart = + lastSuccess.isAfter(startOfCurrentWeekMidnight); + debugPrint( + '🔍 DEBUG ALERTE - Wallet $wallet basique: $lastSuccess (après début semaine: $isAfterWeekStart)'); if (!isAfterWeekStart) { - debugPrint('🚨 DEBUG ALERTE - ALERTE: Wallet $wallet pas traité cette semaine (basique)'); + debugPrint( + '🚨 DEBUG ALERTE - ALERTE: Wallet $wallet pas traité cette semaine (basique)'); return false; // Ce wallet n'a pas été traité cette semaine } } else { - debugPrint('🚨 DEBUG ALERTE - ALERTE: Wallet $wallet sans timestamp de succès (basique)'); + debugPrint( + '🚨 DEBUG ALERTE - ALERTE: Wallet $wallet sans timestamp de succès (basique)'); return false; // Aucun timestamp de succès pour ce wallet } } - + debugPrint('✅ DEBUG ALERTE - Tous les wallets OK, pas d\'alerte'); return true; // Tous les wallets ont été traités avec succès } catch (e) { @@ -352,22 +375,22 @@ class DashboardPageState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return const SizedBox.shrink(); // Pas d'icône pendant le chargement } - + final bool allWalletsOk = snapshot.data ?? true; - + if (allWalletsOk) { return const SizedBox.shrink(); // Pas d'icône si tout va bien } - + // Afficher l'icône d'alerte return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.9), + color: Colors.orange.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.orange.withOpacity(0.3), + color: Colors.orange.withValues(alpha: 0.3), blurRadius: 4, offset: const Offset(0, 2), ), @@ -385,7 +408,9 @@ class DashboardPageState extends State { Text( 'Problème sync', style: TextStyle( - fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 11 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: Colors.white, ), @@ -405,11 +430,13 @@ class DashboardPageState extends State { final appState = Provider.of(context); final lastRentReceived = _getLastRentReceived(dataManager); - final totalRentReceived = _getTotalRentReceived(dataManager, currencyUtils, appState); + final totalRentReceived = + _getTotalRentReceived(dataManager, currencyUtils, appState); final timeElapsed = _getTimeElapsedSinceFirstRent(dataManager); - + // Vérifier si des données sont en cours de mise à jour pour les shimmers - final bool shouldShowShimmers = _isPageLoading || dataManager.isUpdatingData; + final bool shouldShowShimmers = + _isPageLoading || dataManager.isUpdatingData; return Scaffold( backgroundColor: Theme.of(context).brightness == Brightness.light @@ -433,7 +460,10 @@ class DashboardPageState extends State { child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Padding( - padding: EdgeInsets.only(top: UIUtils.getAppBarHeight(context), left: 12.0, right: 12.0), + padding: EdgeInsets.only( + top: UIUtils.getAppBarHeight(context), + left: 12.0, + right: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -444,7 +474,14 @@ class DashboardPageState extends State { children: [ Text( S.of(context).hello, - style: TextStyle(fontSize: 28 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.5, color: Theme.of(context).textTheme.bodyLarge?.color), + style: TextStyle( + fontSize: 28 + appState.getTextSizeOffset(), + fontWeight: FontWeight.bold, + letterSpacing: -0.5, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color), ), if (kIsWeb) IconButton( @@ -463,16 +500,18 @@ class DashboardPageState extends State { }); }, ), - ], - ), - ), - const SizedBox(height: 8), + ], + ), + ), + const SizedBox(height: 8), Container( margin: EdgeInsets.only(bottom: 12.0), decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Theme.of(context).primaryColor.withOpacity(0.8), + Theme.of(context) + .primaryColor + .withValues(alpha: 0.8), Theme.of(context).primaryColor, ], begin: Alignment.topCenter, @@ -482,7 +521,9 @@ class DashboardPageState extends State { borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: Theme.of(context).primaryColor.withOpacity(0.2), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.2), blurRadius: 5, offset: const Offset(0, 4), spreadRadius: -2, @@ -501,94 +542,116 @@ class DashboardPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), + color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), ), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).lastRentReceived, style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), + fontSize: 13 + + appState.getTextSizeOffset(), color: Colors.white70, letterSpacing: -0.2, ), ), const SizedBox(height: 2), - dataManager.isLoadingMain || shouldShowShimmers - ? ShimmerUtils.originalColorShimmer( - child: Text( + dataManager.isLoadingMain || + shouldShowShimmers + ? ShimmerUtils + .originalColorShimmer( + child: Text( + lastRentReceived, + style: TextStyle( + fontSize: 20 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.bold, + color: Colors.white, + ), + ), + color: Colors.white, + ) + : Text( lastRentReceived, style: TextStyle( - fontSize: 20 + appState.getTextSizeOffset(), + fontSize: 20 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.white, ), ), - color: Colors.white, - ) - : Text( - lastRentReceived, - style: TextStyle( - fontSize: 20 + appState.getTextSizeOffset(), - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.end, children: [ Text( 'Total des loyers', style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), + fontSize: 13 + + appState.getTextSizeOffset(), color: Colors.white70, letterSpacing: -0.2, ), ), const SizedBox(height: 2), - dataManager.isLoadingMain || shouldShowShimmers - ? ShimmerUtils.originalColorShimmer( - child: Text( + dataManager.isLoadingMain || + shouldShowShimmers + ? ShimmerUtils + .originalColorShimmer( + child: Text( + totalRentReceived, + style: TextStyle( + fontSize: 20 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.bold, + color: Colors.white, + ), + ), + color: Colors.white, + ) + : Text( totalRentReceived, style: TextStyle( - fontSize: 20 + appState.getTextSizeOffset(), + fontSize: 20 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.white, ), ), - color: Colors.white, - ) - : Text( - totalRentReceived, - style: TextStyle( - fontSize: 20 + appState.getTextSizeOffset(), - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), ], ), ], ), ), const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ // Icône d'alerte à gauche _buildWalletAlertIcon(), // Informations calendrier à droite Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), + color: Colors.white + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -602,7 +665,8 @@ class DashboardPageState extends State { Text( 'Depuis $timeElapsed', style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), + fontSize: 12 + + appState.getTextSizeOffset(), fontWeight: FontWeight.w500, color: Colors.white, ), @@ -633,26 +697,36 @@ class DashboardPageState extends State { context: context, ), const SizedBox(height: 8), - TokensCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + TokensCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), const SizedBox(height: 8), // Cartes par type de produit en ligne Row( children: [ Expanded( - child: RealEstateCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: RealEstateCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), const SizedBox(width: 4), Expanded( - child: LoanIncomeCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: LoanIncomeCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), const SizedBox(width: 4), Expanded( - child: FactoringCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: FactoringCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), ], ), const SizedBox(height: 8), - RentsCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + RentsCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ], ), ), @@ -662,9 +736,13 @@ class DashboardPageState extends State { Expanded( child: Column( children: [ - RmmCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + RmmCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), const SizedBox(height: 8), - NextRondaysCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + NextRondaysCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ], ), ), @@ -678,30 +756,44 @@ class DashboardPageState extends State { context: context, ), const SizedBox(height: 8), - RmmCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + RmmCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), const SizedBox(height: 8), - TokensCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + TokensCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), const SizedBox(height: 8), // Cartes par type de produit en ligne sur tous les écrans Row( children: [ Expanded( - child: RealEstateCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: RealEstateCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), const SizedBox(width: 4), Expanded( - child: LoanIncomeCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: LoanIncomeCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), const SizedBox(width: 4), Expanded( - child: FactoringCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + child: FactoringCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ), ], ), const SizedBox(height: 8), - RentsCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + RentsCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), const SizedBox(height: 8), - NextRondaysCard(showAmounts: appState.showAmounts, isLoading: shouldShowShimmers), + NextRondaysCard( + showAmounts: appState.showAmounts, + isLoading: shouldShowShimmers), ], ), const SizedBox(height: 80), @@ -720,7 +812,9 @@ class DashboardPageState extends State { margin: EdgeInsets.symmetric(vertical: 12.0), width: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Color(0xFFE5F2FF) : Color(0xFF0A3060), + color: Theme.of(context).brightness == Brightness.light + ? Color(0xFFE5F2FF) + : Color(0xFF0A3060), borderRadius: BorderRadius.circular(16), ), child: Padding( @@ -732,9 +826,12 @@ class DashboardPageState extends State { Text( S.of(context).noDataAvailable, style: TextStyle( - fontSize: 17 + Provider.of(context).getTextSizeOffset(), + fontSize: + 17 + Provider.of(context).getTextSizeOffset(), fontWeight: FontWeight.bold, - color: Theme.of(context).brightness == Brightness.light ? Color(0xFF007AFF) : Colors.white, + color: Theme.of(context).brightness == Brightness.light + ? Color(0xFF007AFF) + : Colors.white, ), textAlign: TextAlign.center, ), @@ -781,21 +878,25 @@ class DashboardPageState extends State { return S.of(context).noRentReceived; } - rentData.sort((a, b) => DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); + rentData.sort((a, b) => + DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); final lastRent = rentData.first['rent']; // Utiliser _getFormattedAmount pour masquer ou afficher la valeur - return currencyUtils.getFormattedAmount(currencyUtils.convert(lastRent), currencyUtils.currencySymbol, appState.showAmounts); + return currencyUtils.getFormattedAmount(currencyUtils.convert(lastRent), + currencyUtils.currencySymbol, appState.showAmounts); } // Récupère le total des loyers reçus avec gestion du chargement - String _getTotalRentReceived(DataManager dataManager, CurrencyProvider currencyUtils, AppState appState) { + String _getTotalRentReceived(DataManager dataManager, + CurrencyProvider currencyUtils, AppState appState) { // Si les données sont en cours de chargement, retourner un placeholder if (dataManager.isLoadingMain || _isPageLoading) { return "---"; } final totalRent = dataManager.getTotalRentReceived(); - return currencyUtils.getFormattedAmount(currencyUtils.convert(totalRent), currencyUtils.currencySymbol, appState.showAmounts); + return currencyUtils.getFormattedAmount(currencyUtils.convert(totalRent), + currencyUtils.currencySymbol, appState.showAmounts); } } diff --git a/lib/pages/dashboard/detailsPages/portfolio_details_page.dart b/lib/pages/dashboard/detailsPages/portfolio_details_page.dart index 4ce0915..a4ca8d1 100644 --- a/lib/pages/dashboard/detailsPages/portfolio_details_page.dart +++ b/lib/pages/dashboard/detailsPages/portfolio_details_page.dart @@ -4,11 +4,6 @@ import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; -import 'package:realtoken_asset_tracker/utils/text_utils.dart'; -import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; -import 'package:realtoken_asset_tracker/managers/apy_manager.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/services.dart'; // Pour Clipboard import 'dart:ui'; // Pour les effets de flou @@ -24,7 +19,8 @@ class PortfolioDetailsPage extends StatelessWidget { // Récupérer les données par wallet depuis la nouvelle structure final List> walletDetails = dataManager.walletStats; - final List> perWalletBalances = dataManager.perWalletBalances; + final List> perWalletBalances = + dataManager.perWalletBalances; // Associer les informations d'emprunt et de dépôt à chaque wallet for (var wallet in walletDetails) { @@ -78,9 +74,11 @@ class PortfolioDetailsPage extends StatelessWidget { physics: const BouncingScrollPhysics(), // Style iOS de défilement slivers: [ SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 10.0), sliver: SliverToBoxAdapter( - child: _buildGlobalInfo(context, dataManager, appState, currencyUtils), + child: _buildGlobalInfo( + context, dataManager, appState, currencyUtils), ), ), SliverPadding( @@ -89,7 +87,8 @@ class PortfolioDetailsPage extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, index) => Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: _buildWalletCard(context, walletDetails[index], dataManager, appState, currencyUtils), + child: _buildWalletCard(context, walletDetails[index], + dataManager, appState, currencyUtils), ), childCount: walletDetails.length, ), @@ -102,14 +101,15 @@ class PortfolioDetailsPage extends StatelessWidget { ); } - Widget _buildGlobalInfo(BuildContext context, DataManager dataManager, AppState appState, CurrencyProvider currencyUtils) { + Widget _buildGlobalInfo(BuildContext context, DataManager dataManager, + AppState appState, CurrencyProvider currencyUtils) { final theme = Theme.of(context); return Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ - theme.primaryColor.withOpacity(0.9), + theme.primaryColor.withValues(alpha: 0.9), theme.primaryColor, ], begin: Alignment.topCenter, @@ -119,7 +119,7 @@ class PortfolioDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: theme.primaryColor.withOpacity(0.2), + color: theme.primaryColor.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 4), spreadRadius: -2, @@ -147,7 +147,7 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), + color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -160,7 +160,11 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoRow( S.of(context).totalPortfolio, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.totalWalletValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils + .convert(dataManager.totalWalletValue), + currencyUtils.currencySymbol, + true), context, appState, isWhite: true, @@ -169,7 +173,11 @@ class PortfolioDetailsPage extends StatelessWidget { const SizedBox(height: 12), _buildInfoRow( S.of(context).wallet, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.walletValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils + .convert(dataManager.walletValue), + currencyUtils.currencySymbol, + true), context, appState, isWhite: true, @@ -177,7 +185,10 @@ class PortfolioDetailsPage extends StatelessWidget { const SizedBox(height: 12), _buildInfoRow( S.of(context).rmm, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.rmmValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager.rmmValue), + currencyUtils.currencySymbol, + true), context, appState, isWhite: true, @@ -189,7 +200,12 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoRow( S.of(context).depositBalance, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.totalUsdcDepositBalance + dataManager.totalXdaiDepositBalance), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager + .totalUsdcDepositBalance + + dataManager.totalXdaiDepositBalance), + currencyUtils.currencySymbol, + true), context, appState, isWhite: true, @@ -197,7 +213,12 @@ class PortfolioDetailsPage extends StatelessWidget { const SizedBox(height: 12), _buildInfoRow( S.of(context).borrowBalance, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.totalUsdcBorrowBalance + dataManager.totalXdaiBorrowBalance), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert( + dataManager.totalUsdcBorrowBalance + + dataManager.totalXdaiBorrowBalance), + currencyUtils.currencySymbol, + true), context, appState, isWhite: true, @@ -250,7 +271,12 @@ class PortfolioDetailsPage extends StatelessWidget { ); } - Widget _buildWalletCard(BuildContext context, Map wallet, DataManager dataManager, AppState appState, CurrencyProvider currencyUtils) { + Widget _buildWalletCard( + BuildContext context, + Map wallet, + DataManager dataManager, + AppState appState, + CurrencyProvider currencyUtils) { final theme = Theme.of(context); final String address = wallet['address'] as String; @@ -267,7 +293,12 @@ class PortfolioDetailsPage extends StatelessWidget { final double rmmTokensSum = wallet['rmmTokensSum'] as double? ?? 0; // Calculer le total pour ce wallet - final double totalWalletValue = walletValue + rmmValue + usdcDeposit + xdaiDeposit - usdcBorrow - xdaiBorrow; + final double totalWalletValue = walletValue + + rmmValue + + usdcDeposit + + xdaiDeposit - + usdcBorrow - + xdaiBorrow; return Container( decoration: BoxDecoration( @@ -275,7 +306,9 @@ class PortfolioDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.05) : Colors.black.withOpacity(0.2), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 4), ), @@ -294,7 +327,7 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: theme.primaryColor.withOpacity(0.1), + color: theme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -316,13 +349,18 @@ class PortfolioDetailsPage extends StatelessWidget { ], ), IconButton( - icon: Icon(Icons.copy, color: theme.brightness == Brightness.light ? Colors.grey : Colors.grey.shade400), + icon: Icon(Icons.copy, + color: theme.brightness == Brightness.light + ? Colors.grey + : Colors.grey.shade400), onPressed: () { _copyToClipboard(context, address); }, tooltip: S.of(context).copyAddress, style: IconButton.styleFrom( - backgroundColor: theme.brightness == Brightness.light ? Colors.grey.withOpacity(0.1) : Colors.grey.shade800.withOpacity(0.3), + backgroundColor: theme.brightness == Brightness.light + ? Colors.grey.withValues(alpha: 0.1) + : Colors.grey.shade800.withValues(alpha: 0.3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -334,15 +372,21 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: theme.brightness == Brightness.light ? Colors.grey.shade50 : theme.cardColor.withOpacity(0.7), + color: theme.brightness == Brightness.light + ? Colors.grey.shade50 + : theme.cardColor.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(16), border: Border.all( - color: theme.brightness == Brightness.light ? Colors.grey.shade200 : theme.dividerColor, + color: theme.brightness == Brightness.light + ? Colors.grey.shade200 + : theme.dividerColor, width: 1.0, ), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.02) : Colors.black.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.02) + : Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -353,7 +397,10 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoRow( S.of(context).totalValue, - currencyUtils.getFormattedAmount(currencyUtils.convert(totalWalletValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalWalletValue), + currencyUtils.currencySymbol, + true), context, appState, isBold: true, @@ -365,14 +412,20 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoWithIcon( S.of(context).wallet, - currencyUtils.getFormattedAmount(currencyUtils.convert(walletValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(walletValue), + currencyUtils.currencySymbol, + true), Icons.account_balance_wallet, context, appState, ), _buildInfoWithIcon( S.of(context).rmm, - currencyUtils.getFormattedAmount(currencyUtils.convert(rmmValue), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(rmmValue), + currencyUtils.currencySymbol, + true), Icons.pie_chart_outline, context, appState, @@ -387,15 +440,21 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: theme.brightness == Brightness.light ? Colors.grey.shade50 : theme.cardColor.withOpacity(0.7), + color: theme.brightness == Brightness.light + ? Colors.grey.shade50 + : theme.cardColor.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(16), border: Border.all( - color: theme.brightness == Brightness.light ? Colors.grey.shade200 : theme.dividerColor, + color: theme.brightness == Brightness.light + ? Colors.grey.shade200 + : theme.dividerColor, width: 1.0, ), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.02) : Colors.black.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.02) + : Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -464,15 +523,21 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: theme.brightness == Brightness.light ? Colors.grey.shade50 : theme.cardColor.withOpacity(0.7), + color: theme.brightness == Brightness.light + ? Colors.grey.shade50 + : theme.cardColor.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(16), border: Border.all( - color: theme.brightness == Brightness.light ? Colors.grey.shade200 : theme.dividerColor, + color: theme.brightness == Brightness.light + ? Colors.grey.shade200 + : theme.dividerColor, width: 1.0, ), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.02) : Colors.black.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.02) + : Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -498,7 +563,10 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoRow( "USDC Dépôt", - currencyUtils.getFormattedAmount(currencyUtils.convert(usdcDeposit), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(usdcDeposit), + currencyUtils.currencySymbol, + true), context, appState, valueColor: Colors.green.shade700, @@ -506,7 +574,10 @@ class PortfolioDetailsPage extends StatelessWidget { const SizedBox(height: 8), _buildInfoRow( "XDAI Dépôt", - currencyUtils.getFormattedAmount(currencyUtils.convert(xdaiDeposit), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(xdaiDeposit), + currencyUtils.currencySymbol, + true), context, appState, valueColor: Colors.green.shade700, @@ -518,7 +589,10 @@ class PortfolioDetailsPage extends StatelessWidget { children: [ _buildInfoRow( "USDC Emprunt", - currencyUtils.getFormattedAmount(currencyUtils.convert(usdcBorrow), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(usdcBorrow), + currencyUtils.currencySymbol, + true), context, appState, valueColor: Colors.red.shade700, @@ -526,7 +600,10 @@ class PortfolioDetailsPage extends StatelessWidget { const SizedBox(height: 8), _buildInfoRow( "XDAI Emprunt", - currencyUtils.getFormattedAmount(currencyUtils.convert(xdaiBorrow), currencyUtils.currencySymbol, true), + currencyUtils.getFormattedAmount( + currencyUtils.convert(xdaiBorrow), + currencyUtils.currencySymbol, + true), context, appState, valueColor: Colors.red.shade700, @@ -571,7 +648,12 @@ class PortfolioDetailsPage extends StatelessWidget { ); } - Widget _buildInfoRow(String label, String value, BuildContext context, AppState appState, {bool isBold = false, bool isWhite = false, Color? valueColor, double textSize = 14}) { + Widget _buildInfoRow( + String label, String value, BuildContext context, AppState appState, + {bool isBold = false, + bool isWhite = false, + Color? valueColor, + double textSize = 14}) { final theme = Theme.of(context); return Row( @@ -581,7 +663,8 @@ class PortfolioDetailsPage extends StatelessWidget { style: TextStyle( fontSize: textSize + appState.getTextSizeOffset(), fontWeight: isBold ? FontWeight.w700 : FontWeight.w500, - color: valueColor ?? (isWhite ? Colors.white : theme.textTheme.bodyLarge?.color), + color: valueColor ?? + (isWhite ? Colors.white : theme.textTheme.bodyLarge?.color), ), ), const SizedBox(width: 8), @@ -597,7 +680,9 @@ class PortfolioDetailsPage extends StatelessWidget { ); } - Widget _buildInfoWithIcon(String label, String value, IconData icon, BuildContext context, AppState appState, {bool isWhite = false}) { + Widget _buildInfoWithIcon(String label, String value, IconData icon, + BuildContext context, AppState appState, + {bool isWhite = false}) { final theme = Theme.of(context); return Row( @@ -605,7 +690,9 @@ class PortfolioDetailsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isWhite ? Colors.white24 : theme.primaryColor.withOpacity(0.1), + color: isWhite + ? Colors.white24 + : theme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -623,14 +710,17 @@ class PortfolioDetailsPage extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: isWhite ? Colors.white : theme.textTheme.bodyLarge?.color, + color: + isWhite ? Colors.white : theme.textTheme.bodyLarge?.color, ), ), Text( label, style: TextStyle( fontSize: 12, - color: isWhite ? Colors.white70 : theme.textTheme.bodyMedium?.color, + color: isWhite + ? Colors.white70 + : theme.textTheme.bodyMedium?.color, ), ), ], diff --git a/lib/pages/dashboard/detailsPages/properties_details_page.dart b/lib/pages/dashboard/detailsPages/properties_details_page.dart index 244f069..5d555ce 100644 --- a/lib/pages/dashboard/detailsPages/properties_details_page.dart +++ b/lib/pages/dashboard/detailsPages/properties_details_page.dart @@ -4,10 +4,9 @@ import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/pages/Statistics/portfolio/charts/token_distribution_chart.dart'; -import 'package:realtoken_asset_tracker/pages/Statistics/portfolio/charts/token_distribution_by_wallet_card.dart'; import 'package:flutter/services.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; -import 'package:fl_chart/fl_chart.dart'; // Nouvelle bibliothèque pour graphiques +// Nouvelle bibliothèque pour graphiques class PropertiesDetailsPage extends StatelessWidget { const PropertiesDetailsPage({super.key}); @@ -19,7 +18,8 @@ class PropertiesDetailsPage extends StatelessWidget { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: isDarkMode ? const Color(0xFF1C1C1E) : const Color(0xFFF2F2F7), + backgroundColor: + isDarkMode ? const Color(0xFF1C1C1E) : const Color(0xFFF2F2F7), appBar: AppBar( title: Text( S.of(context).properties, @@ -28,7 +28,8 @@ class PropertiesDetailsPage extends StatelessWidget { fontSize: 17 + appState.getTextSizeOffset(), ), ), - backgroundColor: isDarkMode ? const Color(0xFF1C1C1E) : const Color(0xFFF2F2F7), + backgroundColor: + isDarkMode ? const Color(0xFF1C1C1E) : const Color(0xFFF2F2F7), elevation: 0, scrolledUnderElevation: 0, ), @@ -51,19 +52,23 @@ class PropertiesDetailsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle(context, S.of(context).overview, appState), + _buildSectionTitle( + context, S.of(context).overview, appState), const SizedBox(height: 12), _buildOverviewCards(context, dataManager, appState), const SizedBox(height: 24), - _buildSectionTitle(context, S.of(context).occupancyRate, appState), + _buildSectionTitle( + context, S.of(context).occupancyRate, appState), const SizedBox(height: 12), _buildOccupancyChart(context, dataManager, appState), const SizedBox(height: 24), - _buildSectionTitle(context, S.of(context).tokenDistribution, appState), + _buildSectionTitle( + context, S.of(context).tokenDistribution, appState), const SizedBox(height: 12), _buildTokenDistribution(context, dataManager), const SizedBox(height: 24), - _buildSectionTitle(context, S.of(context).wallets, appState), + _buildSectionTitle( + context, S.of(context).wallets, appState), ], ), ), @@ -71,7 +76,8 @@ class PropertiesDetailsPage extends StatelessWidget { SliverList( delegate: SliverChildBuilderDelegate( (context, index) { - final walletDetails = _getSortedWalletDetails(dataManager); + final walletDetails = + _getSortedWalletDetails(dataManager); if (walletDetails.isEmpty) { return Padding( padding: const EdgeInsets.all(16.0), @@ -80,7 +86,9 @@ class PropertiesDetailsPage extends StatelessWidget { S.of(context).noWalletsAvailable, style: TextStyle( fontSize: 16 + appState.getTextSizeOffset(), - color: isDarkMode ? Colors.white70 : Colors.black54, + color: isDarkMode + ? Colors.white70 + : Colors.black54, ), ), ), @@ -88,10 +96,13 @@ class PropertiesDetailsPage extends StatelessWidget { } return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: _buildWalletCard(context, walletDetails[index], appState), + child: _buildWalletCard( + context, walletDetails[index], appState), ); }, - childCount: _getSortedWalletDetails(dataManager).isEmpty ? 1 : _getSortedWalletDetails(dataManager).length, + childCount: _getSortedWalletDetails(dataManager).isEmpty + ? 1 + : _getSortedWalletDetails(dataManager).length, ), ), SliverToBoxAdapter( @@ -104,7 +115,8 @@ class PropertiesDetailsPage extends StatelessWidget { List> _getSortedWalletDetails(DataManager dataManager) { final List> walletDetails = dataManager.walletStats; - final List> perWalletBalances = dataManager.perWalletBalances; + final List> perWalletBalances = + dataManager.perWalletBalances; // Associer les informations d'emprunt et de dépôt à chaque wallet for (var wallet in walletDetails) { @@ -136,7 +148,8 @@ class PropertiesDetailsPage extends StatelessWidget { return walletDetails; } - Widget _buildSectionTitle(BuildContext context, String title, AppState appState) { + Widget _buildSectionTitle( + BuildContext context, String title, AppState appState) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Text( @@ -149,7 +162,8 @@ class PropertiesDetailsPage extends StatelessWidget { ); } - Widget _buildOverviewCards(BuildContext context, DataManager dataManager, AppState appState) { + Widget _buildOverviewCards( + BuildContext context, DataManager dataManager, AppState appState) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final cardColor = isDarkMode ? const Color(0xFF2C2C2E) : Colors.white; @@ -197,7 +211,7 @@ class PropertiesDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -233,10 +247,12 @@ class PropertiesDetailsPage extends StatelessWidget { ); } - Widget _buildOccupancyChart(BuildContext context, DataManager dataManager, AppState appState) { + Widget _buildOccupancyChart( + BuildContext context, DataManager dataManager, AppState appState) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final cardColor = isDarkMode ? const Color(0xFF2C2C2E) : Colors.white; - final double rentedPercentage = (dataManager.rentedUnits / dataManager.totalUnits) * 100; + final double rentedPercentage = + (dataManager.rentedUnits / dataManager.totalUnits) * 100; // Déterminer la couleur en fonction du pourcentage Color progressColor; @@ -265,7 +281,7 @@ class PropertiesDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -289,7 +305,9 @@ class PropertiesDetailsPage extends StatelessWidget { ), ), Text( - S.of(context).rentedUnits(dataManager.rentedUnits.toString(), dataManager.totalUnits.toString()), + S.of(context).rentedUnits( + dataManager.rentedUnits.toString(), + dataManager.totalUnits.toString()), style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), color: isDarkMode ? Colors.white70 : Colors.black54, @@ -298,9 +316,10 @@ class PropertiesDetailsPage extends StatelessWidget { ], ), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: progressColor.withOpacity(0.2), + color: progressColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -318,7 +337,9 @@ class PropertiesDetailsPage extends StatelessWidget { Container( height: 10, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.black.withOpacity(0.05), + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(5), ), child: Stack( @@ -328,7 +349,10 @@ class PropertiesDetailsPage extends StatelessWidget { child: Container( decoration: BoxDecoration( gradient: LinearGradient( - colors: [progressColor.withOpacity(0.7), progressColor], + colors: [ + progressColor.withValues(alpha: 0.7), + progressColor + ], begin: Alignment.centerLeft, end: Alignment.centerRight, ), @@ -344,7 +368,8 @@ class PropertiesDetailsPage extends StatelessWidget { ); } - Widget _buildTokenDistribution(BuildContext context, DataManager dataManager) { + Widget _buildTokenDistribution( + BuildContext context, DataManager dataManager) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final cardColor = isDarkMode ? const Color(0xFF2C2C2E) : Colors.white; @@ -355,7 +380,7 @@ class PropertiesDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -366,7 +391,8 @@ class PropertiesDetailsPage extends StatelessWidget { ); } - Widget _buildWalletCard(BuildContext context, Map wallet, AppState appState) { + Widget _buildWalletCard( + BuildContext context, Map wallet, AppState appState) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final cardColor = isDarkMode ? const Color(0xFF2C2C2E) : Colors.white; final String address = wallet['address'] as String; @@ -383,7 +409,7 @@ class PropertiesDetailsPage extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -401,7 +427,8 @@ class PropertiesDetailsPage extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.2), + color: + Theme.of(context).primaryColor.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: Center( @@ -428,7 +455,9 @@ class PropertiesDetailsPage extends StatelessWidget { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.black.withOpacity(0.05), + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -443,7 +472,9 @@ class PropertiesDetailsPage extends StatelessWidget { const SizedBox(height: 16), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(12), @@ -452,14 +483,19 @@ class PropertiesDetailsPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildWalletStat(context, S.of(context).properties, tokenCount.toString(), appState), - _buildWalletStat(context, 'Wallet', walletTokensSum.toStringAsFixed(2), appState), - _buildWalletStat(context, 'RMM', rmmTokensSum.toStringAsFixed(2), appState), + _buildWalletStat(context, S.of(context).properties, + tokenCount.toString(), appState), + _buildWalletStat(context, 'Wallet', + walletTokensSum.toStringAsFixed(2), appState), + _buildWalletStat(context, 'RMM', + rmmTokensSum.toStringAsFixed(2), appState), ], ), const SizedBox(height: 12), Divider( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.black.withOpacity(0.1), + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.1), height: 1, ), const SizedBox(height: 12), @@ -491,7 +527,8 @@ class PropertiesDetailsPage extends StatelessWidget { ); } - Widget _buildWalletStat(BuildContext context, String label, String value, AppState appState) { + Widget _buildWalletStat( + BuildContext context, String label, String value, AppState appState) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Column( diff --git a/lib/pages/dashboard/detailsPages/rent_details_page.dart b/lib/pages/dashboard/detailsPages/rent_details_page.dart index 85c2553..0eb0760 100644 --- a/lib/pages/dashboard/detailsPages/rent_details_page.dart +++ b/lib/pages/dashboard/detailsPages/rent_details_page.dart @@ -4,23 +4,23 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/pages/Statistics/wallet/charts/rent_graph.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/date_utils.dart'; import 'package:share_plus/share_plus.dart'; -import 'dart:ui'; import 'package:realtoken_asset_tracker/app_state.dart'; class DashboardRentsDetailsPage extends StatefulWidget { const DashboardRentsDetailsPage({super.key}); @override - _DashboardRentsDetailsPageState createState() => _DashboardRentsDetailsPageState(); + _DashboardRentsDetailsPageState createState() => + _DashboardRentsDetailsPageState(); } -class _DashboardRentsDetailsPageState extends State with SingleTickerProviderStateMixin { +class _DashboardRentsDetailsPageState extends State + with SingleTickerProviderStateMixin { bool _isGraphVisible = true; late ScrollController _scrollController; late AnimationController _animationController; @@ -44,10 +44,12 @@ class _DashboardRentsDetailsPageState extends State w duration: const Duration(milliseconds: 300), ); - _animation = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); + _animation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); // Initialiser la période par défaut - _selectedRentPeriod = 'Mois'; // ou S.of(context).month si besoin de la traduction + _selectedRentPeriod = + 'Mois'; // ou S.of(context).month si besoin de la traduction } void _onScroll() { @@ -85,7 +87,9 @@ class _DashboardRentsDetailsPageState extends State w 'Détails des loyers', style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), ), ), centerTitle: true, @@ -97,7 +101,7 @@ class _DashboardRentsDetailsPageState extends State w icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: theme.primaryColor.withOpacity(0.1), + color: theme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -108,7 +112,8 @@ class _DashboardRentsDetailsPageState extends State w ), onPressed: () async { if (dataManager.rentData.isNotEmpty) { - final csvData = _generateCSV(dataManager.rentData, currencyUtils); + final csvData = + _generateCSV(dataManager.rentData, currencyUtils); final fileName = 'rents_${_getFormattedToday()}.csv'; final directory = await getTemporaryDirectory(); final filePath = '${directory.path}/$fileName'; @@ -125,7 +130,9 @@ class _DashboardRentsDetailsPageState extends State w } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Aucune donnée de loyer à partager.', style: TextStyle(color: theme.textTheme.bodyLarge?.color)), + content: Text('Aucune donnée de loyer à partager.', + style: + TextStyle(color: theme.textTheme.bodyLarge?.color)), behavior: SnackBarBehavior.floating, backgroundColor: theme.cardColor, shape: RoundedRectangleBorder( @@ -209,13 +216,14 @@ class _DashboardRentsDetailsPageState extends State w SliverToBoxAdapter( child: Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 8), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 20), decoration: BoxDecoration( color: theme.primaryColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: theme.primaryColor.withOpacity(0.2), + color: theme.primaryColor.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 2), ), @@ -250,11 +258,14 @@ class _DashboardRentsDetailsPageState extends State w delegate: SliverChildBuilderDelegate( (context, index) { // Trier les données par date décroissante - final sortedData = List.from(dataManager.rentData) - ..sort((a, b) => DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); - + final sortedData = + List.from(dataManager.rentData) + ..sort((a, b) => DateTime.parse(b['date']) + .compareTo(DateTime.parse(a['date']))); + final rentEntry = sortedData[index]; - final rentDate = CustomDateUtils.formatDate(rentEntry['date']); + final rentDate = + CustomDateUtils.formatDate(rentEntry['date']); final rentAmount = currencyUtils.formatCurrency( currencyUtils.convert(rentEntry['rent']), currencyUtils.currencySymbol, @@ -266,21 +277,25 @@ class _DashboardRentsDetailsPageState extends State w isNewMonth = true; } else { final currentDate = DateTime.parse(rentEntry['date']); - final previousDate = DateTime.parse(sortedData[index - 1]['date']); - isNewMonth = currentDate.year != previousDate.year || currentDate.month != previousDate.month; + final previousDate = + DateTime.parse(sortedData[index - 1]['date']); + isNewMonth = currentDate.year != previousDate.year || + currentDate.month != previousDate.month; } // Si c'est un nouveau mois, ajouter un en-tête if (isNewMonth) { final date = DateTime.parse(rentEntry['date']); - final monthName = DateFormat('MMMM yyyy', 'fr_FR').format(date); + final monthName = + DateFormat('MMMM yyyy', 'fr_FR').format(date); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (index != 0) const SizedBox(height: 16), Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8), + padding: const EdgeInsets.only( + top: 8, bottom: 8, left: 8), child: Text( monthName.toUpperCase(), style: TextStyle( @@ -320,7 +335,9 @@ class _DashboardRentsDetailsPageState extends State w borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.03) : Colors.black.withOpacity(0.15), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.03) + : Colors.black.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), @@ -336,7 +353,7 @@ class _DashboardRentsDetailsPageState extends State w Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: theme.primaryColor.withOpacity(0.1), + color: theme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( @@ -399,8 +416,12 @@ class _DashboardRentsDetailsPageState extends State w String monthKey = DateFormat('yyyy/MM').format(date); groupedData[monthKey] = (groupedData[monthKey] ?? 0) + entry['rent']; } - List> list = groupedData.entries.map((e) => {'date': e.key, 'rent': e.value}).toList(); - list.sort((a, b) => DateFormat('yyyy/MM').parse(a['date']).compareTo(DateFormat('yyyy/MM').parse(b['date']))); + List> list = groupedData.entries + .map((e) => {'date': e.key, 'rent': e.value}) + .toList(); + list.sort((a, b) => DateFormat('yyyy/MM') + .parse(a['date']) + .compareTo(DateFormat('yyyy/MM').parse(b['date']))); return list; } } diff --git a/lib/pages/dashboard/detailsPages/rmm_details_page.dart b/lib/pages/dashboard/detailsPages/rmm_details_page.dart index da3e07c..3572fe7 100644 --- a/lib/pages/dashboard/detailsPages/rmm_details_page.dart +++ b/lib/pages/dashboard/detailsPages/rmm_details_page.dart @@ -4,9 +4,7 @@ import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/text_utils.dart'; -import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; -import 'dart:ui'; class RmmWalletDetailsPage extends StatelessWidget { const RmmWalletDetailsPage({super.key}); @@ -15,23 +13,32 @@ class RmmWalletDetailsPage extends StatelessWidget { Widget build(BuildContext context) { final dataManager = Provider.of(context); final currencyUtils = Provider.of(context, listen: false); - final List> walletDetails = dataManager.perWalletBalances; + final List> walletDetails = + dataManager.perWalletBalances; // Séparer les wallets avec utilisation de ceux sans utilisation - final List> walletsWithUsage = walletDetails.where((wallet) { + final List> walletsWithUsage = + walletDetails.where((wallet) { final double usdcDeposit = wallet['usdcDeposit'] as double? ?? 0; final double xdaiDeposit = wallet['xdaiDeposit'] as double? ?? 0; final double usdcBorrow = wallet['usdcBorrow'] as double? ?? 0; final double xdaiBorrow = wallet['xdaiBorrow'] as double? ?? 0; - return !(usdcDeposit == 0 && xdaiDeposit == 0 && usdcBorrow == 0 && xdaiBorrow == 0); + return !(usdcDeposit == 0 && + xdaiDeposit == 0 && + usdcBorrow == 0 && + xdaiBorrow == 0); }).toList(); - final List> walletsNoUsage = walletDetails.where((wallet) { + final List> walletsNoUsage = + walletDetails.where((wallet) { final double usdcDeposit = wallet['usdcDeposit'] as double? ?? 0; final double xdaiDeposit = wallet['xdaiDeposit'] as double? ?? 0; final double usdcBorrow = wallet['usdcBorrow'] as double? ?? 0; final double xdaiBorrow = wallet['xdaiBorrow'] as double? ?? 0; - return (usdcDeposit == 0 && xdaiDeposit == 0 && usdcBorrow == 0 && xdaiBorrow == 0); + return (usdcDeposit == 0 && + xdaiDeposit == 0 && + usdcBorrow == 0 && + xdaiBorrow == 0); }).toList(); // Trier les wallets avec utilisation par HealthFactor croissant @@ -78,14 +85,21 @@ class RmmWalletDetailsPage extends StatelessWidget { physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: index < walletsWithUsage.length ? _WalletDetailCard(wallet: walletsWithUsage[index], currencyUtils: currencyUtils) : _NoUsageWalletsCard(noUsageWallets: walletsNoUsage), + child: index < walletsWithUsage.length + ? _WalletDetailCard( + wallet: walletsWithUsage[index], + currencyUtils: currencyUtils) + : _NoUsageWalletsCard( + noUsageWallets: walletsNoUsage), ), - childCount: walletsWithUsage.length + (walletsNoUsage.isNotEmpty ? 1 : 0), + childCount: walletsWithUsage.length + + (walletsNoUsage.isNotEmpty ? 1 : 0), ), ), ), @@ -101,7 +115,7 @@ class _WalletDetailCard extends StatelessWidget { final Map wallet; final CurrencyProvider currencyUtils; - const _WalletDetailCard({Key? key, required this.wallet, required this.currencyUtils}) : super(key: key); + const _WalletDetailCard({required this.wallet, required this.currencyUtils}); @override Widget build(BuildContext context) { @@ -120,8 +134,10 @@ class _WalletDetailCard extends StatelessWidget { // Calcul du Health Factor (HF) et du LTV final double walletBorrowSum = usdcBorrow + xdaiBorrow; - final double walletHF = walletBorrowSum > 0 ? (walletRmmValue * 0.7 / walletBorrowSum) : 10; - final double walletLTV = walletRmmValue > 0 ? (walletBorrowSum / walletRmmValue * 100) : 0; + final double walletHF = + walletBorrowSum > 0 ? (walletRmmValue * 0.7 / walletBorrowSum) : 10; + final double walletLTV = + walletRmmValue > 0 ? (walletBorrowSum / walletRmmValue * 100) : 0; // Couleurs pour les health factors final Color hfColor = walletHF < 1.5 @@ -136,7 +152,7 @@ class _WalletDetailCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -155,7 +171,9 @@ class _WalletDetailCard extends StatelessWidget { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -168,7 +186,9 @@ class _WalletDetailCard extends StatelessWidget { Text( TextUtils.truncateWallet(address), style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.5, color: Theme.of(context).primaryColor, @@ -176,7 +196,6 @@ class _WalletDetailCard extends StatelessWidget { ), ], ), - ], ), const SizedBox(height: 20), @@ -213,10 +232,8 @@ class _WalletDetailCard extends StatelessWidget { context, appState, valueColor: Colors.green.shade700, - ), + ), const SizedBox(height: 8), - - ], context, appState, @@ -279,7 +296,8 @@ class _WalletDetailCard extends StatelessWidget { ); } - Widget _buildInfoSection(String title, List children, BuildContext context, AppState appState) { + Widget _buildInfoSection(String title, List children, + BuildContext context, AppState appState) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -304,7 +322,9 @@ class _WalletDetailCard extends StatelessWidget { ); } - Widget _buildInfoItem(String label, String value, BuildContext context, AppState appState, {bool isBold = false, Color? valueColor, double textSize = 14}) { + Widget _buildInfoItem( + String label, String value, BuildContext context, AppState appState, + {bool isBold = false, Color? valueColor, double textSize = 14}) { return Row( children: [ Text( @@ -340,7 +360,8 @@ class _WalletDetailCard extends StatelessWidget { // Définition des couleurs pour la jauge HF en fonction du facteur Color getHFColor(double hfValue) { if (hfValue <= 1.1) { - return Color(0xFFFF3B30); // Rouge pour valeurs dangereuses (HF proche de 1) + return Color( + 0xFFFF3B30); // Rouge pour valeurs dangereuses (HF proche de 1) } else if (hfValue <= 1.5) { return Color(0xFFFF9500); // Orange pour valeurs à risque modéré } else if (hfValue <= 2.5) { @@ -353,7 +374,8 @@ class _WalletDetailCard extends StatelessWidget { // Fonction pour déterminer la couleur de la jauge LTV en fonction de sa valeur Color getLTVColor(double ltvPercent) { if (ltvPercent >= 65) { - return Color(0xFFFF3B30); // Rouge pour valeurs dangereuses (LTV proche de 70%) + return Color( + 0xFFFF3B30); // Rouge pour valeurs dangereuses (LTV proche de 70%) } else if (ltvPercent >= 55) { return Color(0xFFFF9500); // Orange pour valeurs à risque modéré } else if (ltvPercent >= 40) { @@ -399,9 +421,9 @@ class _WalletDetailCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.brightness == Brightness.light - ? Colors.black.withOpacity(0.05) - : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), ), ), // Niveau de la jauge @@ -412,8 +434,12 @@ class _WalletDetailCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: progressHF > 0.95 ? Radius.circular(12) : Radius.zero, - topRight: progressHF > 0.95 ? Radius.circular(12) : Radius.zero, + topLeft: progressHF > 0.95 + ? Radius.circular(12) + : Radius.zero, + topRight: progressHF > 0.95 + ? Radius.circular(12) + : Radius.zero, bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), @@ -429,7 +455,7 @@ class _WalletDetailCard extends StatelessWidget { child: Container( height: 1, width: 20, - color: Colors.white.withOpacity(0.4), + color: Colors.white.withValues(alpha: 0.4), ), ), ], @@ -438,7 +464,7 @@ class _WalletDetailCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: hfGaugeColor.withOpacity(0.15), + color: hfGaugeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( @@ -475,9 +501,9 @@ class _WalletDetailCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.brightness == Brightness.light - ? Colors.black.withOpacity(0.05) - : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), ), ), // Niveau de la jauge @@ -488,8 +514,12 @@ class _WalletDetailCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: progressLTV > 0.95 ? Radius.circular(12) : Radius.zero, - topRight: progressLTV > 0.95 ? Radius.circular(12) : Radius.zero, + topLeft: progressLTV > 0.95 + ? Radius.circular(12) + : Radius.zero, + topRight: progressLTV > 0.95 + ? Radius.circular(12) + : Radius.zero, bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), @@ -505,7 +535,7 @@ class _WalletDetailCard extends StatelessWidget { child: Container( height: 1, width: 20, - color: Colors.white.withOpacity(0.4), + color: Colors.white.withValues(alpha: 0.4), ), ), ], @@ -514,7 +544,7 @@ class _WalletDetailCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: ltvGaugeColor.withOpacity(0.15), + color: ltvGaugeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( @@ -536,14 +566,18 @@ class _WalletDetailCard extends StatelessWidget { class _NoUsageWalletsCard extends StatelessWidget { final List> noUsageWallets; - const _NoUsageWalletsCard({Key? key, required this.noUsageWallets}) : super(key: key); + const _NoUsageWalletsCard({required this.noUsageWallets}); @override Widget build(BuildContext context) { final appState = Provider.of(context); // Récupérer les adresses complètes sans tronquage, chaque adresse sur une nouvelle ligne - final List addresses = noUsageWallets.map((wallet) => TextUtils.truncateWallet(wallet['address']) as String? ?? S.of(context).unknown).toList(); + final List addresses = noUsageWallets + .map((wallet) => + TextUtils.truncateWallet(wallet['address']) as String? ?? + S.of(context).unknown) + .toList(); return Container( decoration: BoxDecoration( @@ -551,7 +585,7 @@ class _NoUsageWalletsCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -580,7 +614,9 @@ class _NoUsageWalletsCard extends StatelessWidget { Text( S.of(context).walletsWithoutRmmUsage, style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: Theme.of(context).textTheme.bodyLarge?.color, ), @@ -606,14 +642,25 @@ class _NoUsageWalletsCard extends StatelessWidget { Icon( Icons.circle, size: 8, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), const SizedBox(width: 8), Text( - appState.showAmounts ? address : TextUtils.truncateWallet(address), + appState.showAmounts + ? address + : TextUtils.truncateWallet(address), style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color, + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), ), ], diff --git a/lib/pages/dashboard/widgets/factoring_card.dart b/lib/pages/dashboard/widgets/factoring_card.dart index 01ac8b4..57669aa 100644 --- a/lib/pages/dashboard/widgets/factoring_card.dart +++ b/lib/pages/dashboard/widgets/factoring_card.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; @@ -13,7 +12,8 @@ class FactoringCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const FactoringCard({super.key, required this.showAmounts, required this.isLoading}); + const FactoringCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -22,14 +22,17 @@ class FactoringCard extends StatelessWidget { final currencyUtils = Provider.of(context, listen: false); // Filtrer les tokens de type factoring_profitshare - final factoringTokens = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'factoring_profitshare' - ).toList(); + final factoringTokens = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'factoring_profitshare') + .toList(); final totalTokens = factoringTokens.length; - final totalValue = factoringTokens.fold(0.0, (sum, token) => - sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0) - ); + final totalValue = factoringTokens.fold( + 0.0, + (sum, token) => + sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0)); // Filtrer selon rentStartDate pour ne prendre que les tokens qui génèrent déjà des revenus final today = DateTime.now(); final monthlyIncome = factoringTokens.fold(0.0, (sum, token) { @@ -47,15 +50,11 @@ class FactoringCard extends StatelessWidget { 'Factoring', Icons.business_center_outlined, _buildValueWithIconSmall( - context, - currencyUtils.getFormattedAmount( - currencyUtils.convert(monthlyIncome), - currencyUtils.currencySymbol, - showAmounts - ), - Icons.attach_money_rounded, - isLoading - ), + context, + currencyUtils.getFormattedAmount(currencyUtils.convert(monthlyIncome), + currencyUtils.currencySymbol, showAmounts), + Icons.attach_money_rounded, + isLoading), [ _buildTextWithShimmerSmall( '$totalTokens', @@ -64,9 +63,15 @@ class FactoringCard extends StatelessWidget { context, ), _buildTextWithShimmerSmall( - showAmounts - ? _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol) - : '*' * _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol).length, + showAmounts + ? _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + : '*' * + _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + .length, 'Total', isLoading, context, @@ -92,7 +97,8 @@ class FactoringCard extends StatelessWidget { return formatter.format(value.round()); } - Widget _buildPieChart(int totalTokens, double totalValue, BuildContext context) { + Widget _buildPieChart( + int totalTokens, double totalValue, BuildContext context) { // Donut désactivé (gris) pour maintenir la cohérence visuelle // mais indiquer qu'il ne représente pas d'information utile return SizedBox( @@ -108,7 +114,9 @@ class FactoringCard extends StatelessWidget { title: '—', radius: 18, titleStyle: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.grey.shade600, ), @@ -122,7 +130,8 @@ class FactoringCard extends StatelessWidget { ); } - Widget _buildValueBeforeTextSmall(BuildContext context, String? value, String text, bool isLoading) { + Widget _buildValueBeforeTextSmall( + BuildContext context, String? value, String text, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -163,7 +172,8 @@ class FactoringCard extends StatelessWidget { ); } - Widget _buildTextWithShimmerSmall(String? value, String text, bool isLoading, BuildContext context) { + Widget _buildTextWithShimmerSmall( + String? value, String text, bool isLoading, BuildContext context) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -173,10 +183,12 @@ class FactoringCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( - text, + text, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, letterSpacing: -0.2, height: 1.1, ), @@ -185,7 +197,7 @@ class FactoringCard extends StatelessWidget { isLoading ? ShimmerUtils.originalColorShimmer( child: Text( - value ?? '', + value ?? '', style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, @@ -197,7 +209,7 @@ class FactoringCard extends StatelessWidget { color: theme.textTheme.bodyLarge?.color, ) : Text( - value ?? '', + value ?? '', style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, @@ -211,7 +223,8 @@ class FactoringCard extends StatelessWidget { ); } - Widget _buildValueWithIconSmall(BuildContext context, String? value, IconData icon, bool isLoading) { + Widget _buildValueWithIconSmall( + BuildContext context, String? value, IconData icon, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -251,4 +264,4 @@ class FactoringCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/dashboard/widgets/loan_income_card.dart b/lib/pages/dashboard/widgets/loan_income_card.dart index 46c049b..891363b 100644 --- a/lib/pages/dashboard/widgets/loan_income_card.dart +++ b/lib/pages/dashboard/widgets/loan_income_card.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; @@ -13,7 +12,8 @@ class LoanIncomeCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const LoanIncomeCard({super.key, required this.showAmounts, required this.isLoading}); + const LoanIncomeCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -22,14 +22,16 @@ class LoanIncomeCard extends StatelessWidget { final currencyUtils = Provider.of(context, listen: false); // Filtrer les tokens de type loan_income - final loanTokens = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'loan_income' - ).toList(); + final loanTokens = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'loan_income') + .toList(); final totalTokens = loanTokens.length; - final totalValue = loanTokens.fold(0.0, (sum, token) => - sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0) - ); + final totalValue = loanTokens.fold( + 0.0, + (sum, token) => + sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0)); // Filtrer selon rentStartDate pour ne prendre que les tokens qui génèrent déjà des revenus final today = DateTime.now(); final monthlyIncome = loanTokens.fold(0.0, (sum, token) { @@ -47,15 +49,11 @@ class LoanIncomeCard extends StatelessWidget { 'Loan', Icons.account_balance_outlined, _buildValueWithIconSmall( - context, - currencyUtils.getFormattedAmount( - currencyUtils.convert(monthlyIncome), - currencyUtils.currencySymbol, - showAmounts - ), - Icons.attach_money_rounded, - isLoading - ), + context, + currencyUtils.getFormattedAmount(currencyUtils.convert(monthlyIncome), + currencyUtils.currencySymbol, showAmounts), + Icons.attach_money_rounded, + isLoading), [ _buildTextWithShimmerSmall( '$totalTokens', @@ -64,9 +62,15 @@ class LoanIncomeCard extends StatelessWidget { context, ), _buildTextWithShimmerSmall( - showAmounts - ? _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol) - : '*' * _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol).length, + showAmounts + ? _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + : '*' * + _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + .length, 'Total', isLoading, context, @@ -92,7 +96,8 @@ class LoanIncomeCard extends StatelessWidget { return formatter.format(value.round()); } - Widget _buildPieChart(int totalTokens, double totalValue, BuildContext context) { + Widget _buildPieChart( + int totalTokens, double totalValue, BuildContext context) { // Donut désactivé (gris) pour maintenir la cohérence visuelle // mais indiquer qu'il ne représente pas d'information utile return SizedBox( @@ -108,7 +113,9 @@ class LoanIncomeCard extends StatelessWidget { title: '—', radius: 18, titleStyle: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.grey.shade600, ), @@ -122,7 +129,8 @@ class LoanIncomeCard extends StatelessWidget { ); } - Widget _buildValueBeforeTextSmall(BuildContext context, String? value, String text, bool isLoading) { + Widget _buildValueBeforeTextSmall( + BuildContext context, String? value, String text, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -163,7 +171,8 @@ class LoanIncomeCard extends StatelessWidget { ); } - Widget _buildTextWithShimmerSmall(String? value, String text, bool isLoading, BuildContext context) { + Widget _buildTextWithShimmerSmall( + String? value, String text, bool isLoading, BuildContext context) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -173,10 +182,12 @@ class LoanIncomeCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( - text, + text, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, letterSpacing: -0.2, height: 1.1, ), @@ -185,7 +196,7 @@ class LoanIncomeCard extends StatelessWidget { isLoading ? ShimmerUtils.originalColorShimmer( child: Text( - value ?? '', + value ?? '', style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, @@ -197,7 +208,7 @@ class LoanIncomeCard extends StatelessWidget { color: theme.textTheme.bodyLarge?.color, ) : Text( - value ?? '', + value ?? '', style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, @@ -211,7 +222,8 @@ class LoanIncomeCard extends StatelessWidget { ); } - Widget _buildValueWithIconSmall(BuildContext context, String? value, IconData icon, bool isLoading) { + Widget _buildValueWithIconSmall( + BuildContext context, String? value, IconData icon, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -251,4 +263,4 @@ class LoanIncomeCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/dashboard/widgets/next_rondays_card.dart b/lib/pages/dashboard/widgets/next_rondays_card.dart index ae44009..49da53c 100644 --- a/lib/pages/dashboard/widgets/next_rondays_card.dart +++ b/lib/pages/dashboard/widgets/next_rondays_card.dart @@ -7,13 +7,13 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'package:shimmer/shimmer.dart'; class NextRondaysCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const NextRondaysCard({super.key, required this.showAmounts, required this.isLoading}); + const NextRondaysCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -32,7 +32,8 @@ class NextRondaysCard extends StatelessWidget { ); } - Widget _buildCumulativeRentHeader(BuildContext context, DataManager dataManager) { + Widget _buildCumulativeRentHeader( + BuildContext context, DataManager dataManager) { final theme = Theme.of(context); final currencyUtils = Provider.of(context, listen: false); @@ -51,7 +52,9 @@ class NextRondaysCard extends StatelessWidget { child: Text( S.of(context).noScheduledRonday, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, color: theme.textTheme.bodyLarge?.color, @@ -85,7 +88,9 @@ class NextRondaysCard extends StatelessWidget { Text( S.of(context).nextRondayInDays(daysRemaining), style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.3, color: theme.textTheme.bodyLarge?.color, @@ -95,10 +100,14 @@ class NextRondaysCard extends StatelessWidget { Text( "${DateFormat('dd MMM yyyy').format(nextDate)} · ${currencyUtils.getFormattedAmount(currencyUtils.convert(nextAmount), currencyUtils.currencySymbol, showAmounts)}", style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w400, letterSpacing: -0.3, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), ], @@ -108,7 +117,8 @@ class NextRondaysCard extends StatelessWidget { ); } - Widget _buildMiniGraphForNextRondays(DataManager dataManager, BuildContext context) { + Widget _buildMiniGraphForNextRondays( + DataManager dataManager, BuildContext context) { final currencyUtils = Provider.of(context, listen: false); final theme = Theme.of(context); final cumulativeRentEvolution = dataManager.getCumulativeRentEvolution(); @@ -147,14 +157,16 @@ class NextRondaysCard extends StatelessWidget { // Si aucune donnée, afficher un placeholder if (graphData.isEmpty) { - return Container( + return SizedBox( height: 100, width: 120, child: Center( child: Icon( Icons.event_busy_rounded, size: 40, - color: theme.brightness == Brightness.light ? Colors.black12 : Colors.white10, + color: theme.brightness == Brightness.light + ? Colors.black12 + : Colors.white10, ), ), ); @@ -172,12 +184,14 @@ class NextRondaysCard extends StatelessWidget { titlesData: FlTitlesData( show: true, topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { - if (value.toInt() >= graphData.length || value.toInt() < 0) { + if (value.toInt() >= graphData.length || + value.toInt() < 0) { return const SizedBox.shrink(); } return Padding( @@ -185,8 +199,12 @@ class NextRondaysCard extends StatelessWidget { child: Text( graphData[value.toInt()]['date'], style: TextStyle( - fontSize: 9 + Provider.of(context, listen: false).getTextSizeOffset(), - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + fontSize: 9 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, letterSpacing: -0.5, ), ), @@ -204,7 +222,8 @@ class NextRondaysCard extends StatelessWidget { x: index, barRods: [ BarChartRodData( - toY: 40.0 + (index * 5), // Hauteur fixe pour éviter les erreurs + toY: 40.0 + + (index * 5), // Hauteur fixe pour éviter les erreurs color: theme.primaryColor, width: 10, borderRadius: BorderRadius.circular(5), @@ -217,17 +236,21 @@ class NextRondaysCard extends StatelessWidget { enabled: true, touchTooltipData: BarTouchTooltipData( getTooltipColor: (group) => Colors.black87, - tooltipPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + tooltipPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 4), tooltipRoundedRadius: 8, getTooltipItem: (group, groupIndex, rod, rodIndex) { if (groupIndex >= 0 && groupIndex < graphData.length) { - double amount = currencyUtils.convert(graphData[groupIndex]['amount']); + double amount = + currencyUtils.convert(graphData[groupIndex]['amount']); return BarTooltipItem( '${amount.toStringAsFixed(2)} ${currencyUtils.currencySymbol}', TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), ), ); } @@ -287,7 +310,9 @@ class NextRondaysCard extends StatelessWidget { Text( S.of(context).calendar, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.3, color: theme.textTheme.titleMedium?.color, @@ -308,7 +333,9 @@ class NextRondaysCard extends StatelessWidget { displayedDates.add(rentStartDate); - String displayDate = rentStartDate == DateTime(3000, 1, 1) ? S.of(context).dateNotCommunicated : DateFormat('dd MMM yyyy').format(rentStartDate); + String displayDate = rentStartDate == DateTime(3000, 1, 1) + ? S.of(context).dateNotCommunicated + : DateFormat('dd MMM yyyy').format(rentStartDate); // Calculer le nombre de jours restants int daysRemaining = rentStartDate.difference(today).inDays; @@ -318,7 +345,9 @@ class NextRondaysCard extends StatelessWidget { margin: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: theme.brightness == Brightness.light ? Color(0xFFF2F2F7) : Colors.black26, + color: theme.brightness == Brightness.light + ? Color(0xFFF2F2F7) + : Colors.black26, borderRadius: BorderRadius.circular(8), ), child: Row( @@ -338,7 +367,9 @@ class NextRondaysCard extends StatelessWidget { Text( displayDate, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, fontWeight: FontWeight.w500, @@ -348,17 +379,26 @@ class NextRondaysCard extends StatelessWidget { Text( S.of(context).daysShort(daysRemaining), style: TextStyle( - fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 11 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.3, - color: theme.brightness == Brightness.light ? Colors.black38 : Colors.white54, + color: theme.brightness == Brightness.light + ? Colors.black38 + : Colors.white54, ), ), ], ), Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(entry['cumulativeRent']), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(entry['cumulativeRent']), + currencyUtils.currencySymbol, + showAmounts), style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.3, color: theme.textTheme.bodyLarge?.color, diff --git a/lib/pages/dashboard/widgets/portfolio_card.dart b/lib/pages/dashboard/widgets/portfolio_card.dart index 599f32b..41787fa 100644 --- a/lib/pages/dashboard/widgets/portfolio_card.dart +++ b/lib/pages/dashboard/widgets/portfolio_card.dart @@ -1,4 +1,3 @@ -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; @@ -8,7 +7,6 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/utils/parameters.dart'; import 'package:realtoken_asset_tracker/utils/widget_factory.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:realtoken_asset_tracker/settings/personalization_settings_page.dart'; import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/portfolio_details_page.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; @@ -41,7 +39,10 @@ class PortfolioCard extends StatelessWidget { dataManager.totalUsdcBorrowBalance - dataManager.totalXdaiBorrowBalance + Parameters.manualAdjustment - : dataManager.walletValue + dataManager.rmmValue + dataManager.rwaHoldingsValue + Parameters.manualAdjustment; + : dataManager.walletValue + + dataManager.rmmValue + + dataManager.rwaHoldingsValue + + Parameters.manualAdjustment; // Calcul du total sans les dépôts et emprunts si showNetTotal est false double portfolioDisplayValue = Parameters.showNetTotal @@ -52,23 +53,31 @@ class PortfolioCard extends StatelessWidget { dataManager.totalUsdcBorrowBalance - dataManager.totalXdaiBorrowBalance + Parameters.manualAdjustment - : dataManager.yamTotalValue + dataManager.rwaHoldingsValue + Parameters.manualAdjustment; + : dataManager.yamTotalValue + + dataManager.rwaHoldingsValue + + Parameters.manualAdjustment; // Calcul du pourcentage de rendement par rapport à l'investissement total - double percentageYam = ((portfolioDisplayValue / totalPortfolioValue - 1) * 100); - String percentageYamDisplay = percentageYam.isNaN || percentageYam.isInfinite ? '0' : percentageYam.toStringAsFixed(0); + double percentageYam = + ((portfolioDisplayValue / totalPortfolioValue - 1) * 100); + String percentageYamDisplay = + percentageYam.isNaN || percentageYam.isInfinite + ? '0' + : percentageYam.toStringAsFixed(0); // Calculer le nombre d'éléments visibles pour estimer la hauteur minimale int visibleSections = 2; // Toujours afficher la section "Actifs" - if (Parameters.showNetTotal) visibleSections++; // Section "Dépôts & Emprunts" - if (Parameters.manualAdjustment != 0) visibleSections++; // Section "Ajustements" + if (Parameters.showNetTotal) + visibleSections++; // Section "Dépôts & Emprunts" + if (Parameters.manualAdjustment != 0) + visibleSections++; // Section "Ajustements" if (Parameters.showTotalInvested) visibleSections++; // Total investi // Hauteur minimale basée sur le nombre de sections visibles double minHeight = 220.0; // Hauteur de base - if (visibleSections <= 2) + if (visibleSections <= 2) { minHeight = 220.0; // Hauteur minimale pour peu de contenu - else if (visibleSections == 3) + } else if (visibleSections == 3) minHeight = 240.0; else minHeight = 260.0; @@ -83,7 +92,10 @@ class PortfolioCard extends StatelessWidget { Parameters.showYamProjection ? UIUtils.buildValueBeforeText( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(portfolioDisplayValue), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(portfolioDisplayValue), + currencyUtils.currencySymbol, + showAmounts), '${S.of(context).projection} YAM ($percentageYamDisplay%)', isLoading, highlightPercentage: true, @@ -91,15 +103,27 @@ class PortfolioCard extends StatelessWidget { : const SizedBox.shrink(), [ // Section des totaux - mise en évidence avec un style particulier - WidgetFactory.buildSectionHeader(context, S.of(context).totalPortfolio), - _buildTotalValue(context, currencyUtils.getFormattedAmount(currencyUtils.convert(totalPortfolioValue), currencyUtils.currencySymbol, showAmounts), isLoading, theme, + WidgetFactory.buildSectionHeader( + context, S.of(context).totalPortfolio), + _buildTotalValue( + context, + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalPortfolioValue), + currencyUtils.currencySymbol, + showAmounts), + isLoading, + theme, showNetLabel: Parameters.showNetTotal), // Ajout du total investi si l'option est activée if (Parameters.showTotalInvested) _buildSubtotalValue( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.initialTotalValue + Parameters.initialInvestmentAdjustment), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager.initialTotalValue + + Parameters.initialInvestmentAdjustment), + currencyUtils.currencySymbol, + showAmounts), S.of(context).initialInvestment, isLoading, theme), @@ -108,26 +132,63 @@ class PortfolioCard extends StatelessWidget { // Section des actifs - avec titre de section WidgetFactory.buildSectionHeader(context, S.of(context).assets), - _buildIndentedBalance(S.of(context).wallet, currencyUtils.convert(dataManager.walletValue), currencyUtils.currencySymbol, true, context, isLoading), - _buildIndentedBalance(S.of(context).rmm, currencyUtils.convert(dataManager.rmmValue), currencyUtils.currencySymbol, true, context, isLoading), - _buildIndentedBalance(S.of(context).rwaHoldings, currencyUtils.convert(dataManager.rwaHoldingsValue), currencyUtils.currencySymbol, true, context, isLoading), + _buildIndentedBalance( + S.of(context).wallet, + currencyUtils.convert(dataManager.walletValue), + currencyUtils.currencySymbol, + true, + context, + isLoading), + _buildIndentedBalance( + S.of(context).rmm, + currencyUtils.convert(dataManager.rmmValue), + currencyUtils.currencySymbol, + true, + context, + isLoading), + _buildIndentedBalance( + S.of(context).rwaHoldings, + currencyUtils.convert(dataManager.rwaHoldingsValue), + currencyUtils.currencySymbol, + true, + context, + isLoading), if (Parameters.showNetTotal) ...[ const SizedBox(height: 3), // Réduit de 6 à 3 // Section des dépôts et emprunts - avec titre de section - WidgetFactory.buildSectionHeader(context, S.of(context).depositsAndLoans), + WidgetFactory.buildSectionHeader( + context, S.of(context).depositsAndLoans), _buildIndentedBalance( - S.of(context).depositBalance, currencyUtils.convert(dataManager.totalUsdcDepositBalance + dataManager.totalXdaiDepositBalance), currencyUtils.currencySymbol, true, context, isLoading), + S.of(context).depositBalance, + currencyUtils.convert(dataManager.totalUsdcDepositBalance + + dataManager.totalXdaiDepositBalance), + currencyUtils.currencySymbol, + true, + context, + isLoading), _buildIndentedBalance( - S.of(context).borrowBalance, currencyUtils.convert(dataManager.totalUsdcBorrowBalance + dataManager.totalXdaiBorrowBalance), currencyUtils.currencySymbol, false, context, isLoading), + S.of(context).borrowBalance, + currencyUtils.convert(dataManager.totalUsdcBorrowBalance + + dataManager.totalXdaiBorrowBalance), + currencyUtils.currencySymbol, + false, + context, + isLoading), ], // Affichage de l'ajustement manuel si différent de zéro if (Parameters.manualAdjustment != 0) ...[ const SizedBox(height: 3), // Réduit de 6 à 3 - WidgetFactory.buildSectionHeader(context, S.of(context).adjustments), + WidgetFactory.buildSectionHeader( + context, S.of(context).adjustments), _buildIndentedBalance( - S.of(context).manualAdjustment, currencyUtils.convert(Parameters.manualAdjustment), currencyUtils.currencySymbol, Parameters.manualAdjustment > 0, context, isLoading), + S.of(context).manualAdjustment, + currencyUtils.convert(Parameters.manualAdjustment), + currencyUtils.currencySymbol, + Parameters.manualAdjustment > 0, + context, + isLoading), ], // Ajouter un espace pour assurer une hauteur minimale si nécessaire @@ -136,7 +197,8 @@ class PortfolioCard extends StatelessWidget { dataManager, context, hasGraph: true, - rightWidget: _buildVerticalGauge(dataManager.roiGlobalValue, context, visibleSections), + rightWidget: _buildVerticalGauge( + dataManager.roiGlobalValue, context, visibleSections), headerRightWidget: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -155,7 +217,9 @@ class PortfolioCard extends StatelessWidget { child: Icon( Icons.settings_outlined, size: 20, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), onTap: () { @@ -164,7 +228,7 @@ class PortfolioCard extends StatelessWidget { ), ), ), - Container( + SizedBox( height: 36, width: 36, child: Material( @@ -177,7 +241,9 @@ class PortfolioCard extends StatelessWidget { child: Icon( Icons.arrow_forward_ios_rounded, size: 16, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), onTap: () { @@ -220,7 +286,7 @@ class PortfolioCard extends StatelessWidget { width: 40, height: 5, decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2.5), ), ), @@ -235,21 +301,28 @@ class PortfolioCard extends StatelessWidget { ); } - - - Widget _buildTotalValue(BuildContext context, String formattedAmount, bool isLoading, ThemeData theme, {bool showNetLabel = false}) { + Widget _buildTotalValue(BuildContext context, String formattedAmount, + bool isLoading, ThemeData theme, + {bool showNetLabel = false}) { final dataManager = Provider.of(context, listen: false); final currencyUtils = Provider.of(context, listen: false); - + // Calculer le montant brut - double grossValue = dataManager.walletValue + dataManager.rmmValue + dataManager.rwaHoldingsValue + Parameters.manualAdjustment; - String grossFormattedAmount = currencyUtils.getFormattedAmount(currencyUtils.convert(grossValue), currencyUtils.currencySymbol, showAmounts); + double grossValue = dataManager.walletValue + + dataManager.rmmValue + + dataManager.rwaHoldingsValue + + Parameters.manualAdjustment; + String grossFormattedAmount = currencyUtils.getFormattedAmount( + currencyUtils.convert(grossValue), + currencyUtils.currencySymbol, + showAmounts); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only(left: 6.0, right: 6.0, top: 1, bottom: 2), + padding: + const EdgeInsets.only(left: 6.0, right: 6.0, top: 1, bottom: 2), child: Row( children: [ isLoading @@ -257,7 +330,9 @@ class PortfolioCard extends StatelessWidget { child: Text( formattedAmount, style: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.5, color: theme.primaryColor, @@ -268,7 +343,9 @@ class PortfolioCard extends StatelessWidget { : Text( formattedAmount, style: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.5, color: theme.primaryColor, @@ -279,13 +356,15 @@ class PortfolioCard extends StatelessWidget { margin: EdgeInsets.only(left: 8), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: theme.primaryColor.withOpacity(0.15), + color: theme.primaryColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( "net", style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, color: theme.primaryColor, ), @@ -302,25 +381,32 @@ class PortfolioCard extends StatelessWidget { Text( grossFormattedAmount, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, - color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), + color: theme.textTheme.bodyMedium?.color + ?.withValues(alpha: 0.7), ), ), Container( margin: EdgeInsets.only(left: 8), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: theme.textTheme.bodyMedium?.color?.withOpacity(0.1), + color: theme.textTheme.bodyMedium?.color + ?.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Text( "brut", style: TextStyle( - fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 11 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, - color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), + color: theme.textTheme.bodyMedium?.color + ?.withValues(alpha: 0.7), ), ), ), @@ -331,7 +417,8 @@ class PortfolioCard extends StatelessWidget { ); } - Widget _buildSubtotalValue(BuildContext context, String formattedAmount, String label, bool isLoading, ThemeData theme) { + Widget _buildSubtotalValue(BuildContext context, String formattedAmount, + String label, bool isLoading, ThemeData theme) { return Padding( padding: const EdgeInsets.only(left: 12.0, top: 1, bottom: 1), child: Row( @@ -341,7 +428,9 @@ class PortfolioCard extends StatelessWidget { child: Text( formattedAmount, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, @@ -352,7 +441,9 @@ class PortfolioCard extends StatelessWidget { : Text( formattedAmount, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, @@ -362,9 +453,11 @@ class PortfolioCard extends StatelessWidget { Text( label, style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.2, - color: theme.textTheme.bodyMedium?.color?.withOpacity(0.8), + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.8), ), ), ], @@ -372,13 +465,17 @@ class PortfolioCard extends StatelessWidget { ); } - Widget _buildIndentedBalance(String label, double value, String symbol, bool isPositive, BuildContext context, bool isLoading) { + Widget _buildIndentedBalance(String label, double value, String symbol, + bool isPositive, BuildContext context, bool isLoading) { final appState = Provider.of(context, listen: false); final currencyUtils = Provider.of(context, listen: false); final theme = Theme.of(context); - String formattedAmount = - showAmounts ? (isPositive ? "+ ${currencyUtils.formatCurrency(value, symbol)}" : "- ${currencyUtils.formatCurrency(value, symbol)}") : (isPositive ? "+ " : "- ") + ('*' * 10); + String formattedAmount = showAmounts + ? (isPositive + ? "+ ${currencyUtils.formatCurrency(value, symbol)}" + : "- ${currencyUtils.formatCurrency(value, symbol)}") + : (isPositive ? "+ " : "- ") + ('*' * 10); Color valueColor = isPositive ? Color(0xFF34C759) // Vert iOS @@ -393,7 +490,9 @@ class PortfolioCard extends StatelessWidget { child: Text( formattedAmount, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.3, color: valueColor, @@ -404,7 +503,9 @@ class PortfolioCard extends StatelessWidget { : Text( formattedAmount, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, letterSpacing: -0.3, color: valueColor, @@ -416,7 +517,9 @@ class PortfolioCard extends StatelessWidget { style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), letterSpacing: -0.2, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), ], @@ -424,7 +527,8 @@ class PortfolioCard extends StatelessWidget { ); } - Widget _buildVerticalGauge(double value, BuildContext context, int visibleSections) { + Widget _buildVerticalGauge( + double value, BuildContext context, int visibleSections) { // Récupérer directement du DataManager pour les calculs final dataManager = Provider.of(context, listen: false); final theme = Theme.of(context); @@ -432,7 +536,8 @@ class PortfolioCard extends StatelessWidget { // Utiliser la méthode d'origine qui calcule le ROI basé sur les loyers reçus // ROI = (Total des loyers reçus / Investissement initial) * 100 double totalRentReceived = dataManager.getTotalRentReceived(); - double initialInvestment = dataManager.initialTotalValue + Parameters.initialInvestmentAdjustment; + double initialInvestment = + dataManager.initialTotalValue + Parameters.initialInvestmentAdjustment; // Vérifier si l'investissement initial est valide pour éviter la division par zéro double displayValue = 0.0; @@ -456,16 +561,17 @@ class PortfolioCard extends StatelessWidget { // Ajuster la hauteur de la jauge en fonction du nombre de sections visibles double gaugeHeight = 90.0; // Hauteur par défaut - if (visibleSections <= 2) + if (visibleSections <= 2) { gaugeHeight = 75.0; - else if (visibleSections == 3) + } else if (visibleSections == 3) gaugeHeight = 120.0; else gaugeHeight = 160.0; - double containerHeight = gaugeHeight + 50; // Ajouter de l'espace pour le texte et les marges + double containerHeight = + gaugeHeight + 50; // Ajouter de l'espace pour le texte et les marges - return Container( + return SizedBox( width: 90, height: containerHeight, child: Column( @@ -477,7 +583,9 @@ class PortfolioCard extends StatelessWidget { Text( 'ROI', style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, @@ -510,7 +618,9 @@ class PortfolioCard extends StatelessWidget { child: Icon( Icons.info_outline, size: 14, - color: theme.brightness == Brightness.light ? Colors.black38 : Colors.white38, + color: theme.brightness == Brightness.light + ? Colors.black38 + : Colors.white38, ), ), ], @@ -521,7 +631,9 @@ class PortfolioCard extends StatelessWidget { width: 26, decoration: BoxDecoration( borderRadius: BorderRadius.circular(13), - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.05) : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), ), child: Align( alignment: Alignment.bottomCenter, @@ -530,8 +642,10 @@ class PortfolioCard extends StatelessWidget { width: 26, decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: progress > 0.95 ? Radius.circular(13) : Radius.zero, - topRight: progress > 0.95 ? Radius.circular(13) : Radius.zero, + topLeft: + progress > 0.95 ? Radius.circular(13) : Radius.zero, + topRight: + progress > 0.95 ? Radius.circular(13) : Radius.zero, bottomLeft: Radius.circular(13), bottomRight: Radius.circular(13), ), @@ -544,13 +658,17 @@ class PortfolioCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: theme.primaryColor.withOpacity(0.15), + color: theme.primaryColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( - displayValue >= 3650 ? "∞" : "${displayValue.toStringAsFixed(1)}%", + displayValue >= 3650 + ? "∞" + : "${displayValue.toStringAsFixed(1)}%", style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: theme.primaryColor, ), diff --git a/lib/pages/dashboard/widgets/properties_card.dart b/lib/pages/dashboard/widgets/properties_card.dart index f5b4868..18126bf 100644 --- a/lib/pages/dashboard/widgets/properties_card.dart +++ b/lib/pages/dashboard/widgets/properties_card.dart @@ -11,7 +11,8 @@ class PropertiesCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const PropertiesCard({super.key, required this.showAmounts, required this.isLoading}); + const PropertiesCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -22,7 +23,11 @@ class PropertiesCard extends StatelessWidget { return UIUtils.buildCard( S.of(context).properties, Icons.business_outlined, - UIUtils.buildValueBeforeText(context, '${(dataManager.rentedUnits / dataManager.totalUnits * 100).toStringAsFixed(2)}%', S.of(context).rented, isLoading), + UIUtils.buildValueBeforeText( + context, + '${(dataManager.rentedUnits / dataManager.totalUnits * 100).toStringAsFixed(2)}%', + S.of(context).rented, + isLoading), [ UIUtils.buildTextWithShimmer( '${dataManager.totalTokenCount}', @@ -54,7 +59,8 @@ class PropertiesCard extends StatelessWidget { title: Text(S.of(context).duplicate_title), content: Text( '${dataManager.duplicateTokenCount.toInt()} ${S.of(context).duplicate}', - style: TextStyle(fontSize: 13 + appState.getTextSizeOffset()), + style: TextStyle( + fontSize: 13 + appState.getTextSizeOffset()), ), actions: [ TextButton( @@ -84,14 +90,15 @@ class PropertiesCard extends StatelessWidget { hasGraph: true, rightWidget: Builder( builder: (context) { - double rentedPercentage = dataManager.rentedUnits / dataManager.totalUnits * 100; + double rentedPercentage = + dataManager.rentedUnits / dataManager.totalUnits * 100; if (rentedPercentage.isNaN || rentedPercentage < 0) { rentedPercentage = 0; } return _buildPieChart(rentedPercentage, context); }, ), - headerRightWidget: Container( + headerRightWidget: SizedBox( height: 36, width: 36, child: Material( @@ -104,7 +111,9 @@ class PropertiesCard extends StatelessWidget { child: Icon( Icons.arrow_forward_ios_rounded, size: 16, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), onTap: () { @@ -136,7 +145,9 @@ class PropertiesCard extends StatelessWidget { title: '', radius: 23, // Taille de la section louée titleStyle: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.white, ), @@ -153,7 +164,7 @@ class PropertiesCard extends StatelessWidget { radius: 17, // Taille de la section non louée gradient: LinearGradient( colors: [ - theme.primaryColor.withOpacity(0.6), + theme.primaryColor.withValues(alpha: 0.6), theme.primaryColor, ], begin: Alignment.topLeft, @@ -162,11 +173,14 @@ class PropertiesCard extends StatelessWidget { ), ], borderData: FlBorderData(show: false), - sectionsSpace: 2, // Un léger espace entre les sections pour les démarquer + sectionsSpace: + 2, // Un léger espace entre les sections pour les démarquer centerSpaceRadius: 23, // Taille de l'espace central ), - swapAnimationDuration: const Duration(milliseconds: 800), // Durée de l'animation - swapAnimationCurve: Curves.easeInOut, // Courbe pour rendre l'animation fluide + swapAnimationDuration: + const Duration(milliseconds: 800), // Durée de l'animation + swapAnimationCurve: + Curves.easeInOut, // Courbe pour rendre l'animation fluide ), ); } diff --git a/lib/pages/dashboard/widgets/real_estate_card.dart b/lib/pages/dashboard/widgets/real_estate_card.dart index df538b7..48844f0 100644 --- a/lib/pages/dashboard/widgets/real_estate_card.dart +++ b/lib/pages/dashboard/widgets/real_estate_card.dart @@ -13,7 +13,8 @@ class RealEstateCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const RealEstateCard({super.key, required this.showAmounts, required this.isLoading}); + const RealEstateCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -23,20 +24,20 @@ class RealEstateCard extends StatelessWidget { final theme = Theme.of(context); // Filtrer les tokens de type real_estate_rental - final realEstateTokens = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'real_estate_rental' - ).toList(); + final realEstateTokens = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'real_estate_rental') + .toList(); final totalTokens = realEstateTokens.length; - final totalUnits = realEstateTokens.fold(0, (sum, token) => - sum + ((token['totalUnits'] as num?)?.toInt() ?? 0) - ); - final rentedUnits = realEstateTokens.fold(0, (sum, token) => - sum + ((token['rentedUnits'] as num?)?.toInt() ?? 0) - ); - final totalValue = realEstateTokens.fold(0.0, (sum, token) => - sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0) - ); + final totalUnits = realEstateTokens.fold( + 0, (sum, token) => sum + ((token['totalUnits'] as num?)?.toInt() ?? 0)); + final rentedUnits = realEstateTokens.fold(0, + (sum, token) => sum + ((token['rentedUnits'] as num?)?.toInt() ?? 0)); + final totalValue = realEstateTokens.fold( + 0.0, + (sum, token) => + sum + ((token['totalValue'] as num?)?.toDouble() ?? 0.0)); // Filtrer selon rentStartDate pour ne prendre que les tokens qui génèrent déjà des revenus final today = DateTime.now(); final monthlyIncome = realEstateTokens.fold(0.0, (sum, token) { @@ -54,15 +55,11 @@ class RealEstateCard extends StatelessWidget { 'Estate', Icons.home_outlined, _buildValueWithIconSmall( - context, - currencyUtils.getFormattedAmount( - currencyUtils.convert(monthlyIncome), - currencyUtils.currencySymbol, - showAmounts - ), - Icons.attach_money_rounded, - isLoading - ), + context, + currencyUtils.getFormattedAmount(currencyUtils.convert(monthlyIncome), + currencyUtils.currencySymbol, showAmounts), + Icons.attach_money_rounded, + isLoading), [ _buildTextWithShimmerSmall( '$totalTokens', @@ -71,9 +68,15 @@ class RealEstateCard extends StatelessWidget { context, ), _buildTextWithShimmerSmall( - showAmounts - ? _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol) - : '*' * _formatCurrencyWithoutDecimals(currencyUtils.convert(totalValue), currencyUtils.currencySymbol).length, + showAmounts + ? _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + : '*' * + _formatCurrencyWithoutDecimals( + currencyUtils.convert(totalValue), + currencyUtils.currencySymbol) + .length, 'Total', isLoading, context, @@ -83,7 +86,8 @@ class RealEstateCard extends StatelessWidget { Center( child: Builder( builder: (context) { - double rentedPercentage = totalUnits > 0 ? (rentedUnits / totalUnits * 100) : 0.0; + double rentedPercentage = + totalUnits > 0 ? (rentedUnits / totalUnits * 100) : 0.0; if (rentedPercentage.isNaN || rentedPercentage < 0) { rentedPercentage = 0; } @@ -124,7 +128,9 @@ class RealEstateCard extends StatelessWidget { title: '', radius: 18, titleStyle: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Colors.white, ), @@ -141,7 +147,7 @@ class RealEstateCard extends StatelessWidget { radius: 13, gradient: LinearGradient( colors: [ - theme.primaryColor.withOpacity(0.6), + theme.primaryColor.withValues(alpha: 0.6), theme.primaryColor, ], begin: Alignment.topLeft, @@ -161,7 +167,9 @@ class RealEstateCard extends StatelessWidget { child: Text( '${rentedPercentage.toStringAsFixed(0)}%', style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color, ), @@ -172,7 +180,8 @@ class RealEstateCard extends StatelessWidget { ); } - Widget _buildValueBeforeTextSmall(BuildContext context, String? value, String text, bool isLoading) { + Widget _buildValueBeforeTextSmall( + BuildContext context, String? value, String text, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -183,7 +192,8 @@ class RealEstateCard extends StatelessWidget { child: Text( value ?? '', style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), // Réduit de 16 à 14 + fontSize: + 14 + appState.getTextSizeOffset(), // Réduit de 16 à 14 fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color, height: 1.1, @@ -194,7 +204,8 @@ class RealEstateCard extends StatelessWidget { : Text( value ?? '', style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), // Réduit de 16 à 14 + fontSize: + 14 + appState.getTextSizeOffset(), // Réduit de 16 à 14 fontWeight: FontWeight.bold, color: theme.textTheme.bodyLarge?.color, height: 1.1, @@ -213,7 +224,8 @@ class RealEstateCard extends StatelessWidget { ); } - Widget _buildTextWithShimmerSmall(String? value, String text, bool isLoading, BuildContext context) { + Widget _buildTextWithShimmerSmall( + String? value, String text, bool isLoading, BuildContext context) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -223,10 +235,12 @@ class RealEstateCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( - text, + text, style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), // Réduit de 14 à 12 - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, letterSpacing: -0.2, height: 1.1, ), @@ -235,9 +249,10 @@ class RealEstateCard extends StatelessWidget { isLoading ? ShimmerUtils.originalColorShimmer( child: Text( - value ?? '', + value ?? '', style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), // Réduit de 15 à 13 + fontSize: 13 + + appState.getTextSizeOffset(), // Réduit de 15 à 13 fontWeight: FontWeight.w600, color: theme.textTheme.bodyLarge?.color, letterSpacing: -0.3, @@ -247,9 +262,10 @@ class RealEstateCard extends StatelessWidget { color: theme.textTheme.bodyLarge?.color, ) : Text( - value ?? '', + value ?? '', style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), // Réduit de 15 à 13 + fontSize: + 13 + appState.getTextSizeOffset(), // Réduit de 15 à 13 fontWeight: FontWeight.w600, color: theme.textTheme.bodyLarge?.color, letterSpacing: -0.3, @@ -261,7 +277,8 @@ class RealEstateCard extends StatelessWidget { ); } - Widget _buildValueWithIconSmall(BuildContext context, String? value, IconData icon, bool isLoading) { + Widget _buildValueWithIconSmall( + BuildContext context, String? value, IconData icon, bool isLoading) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -269,15 +286,26 @@ class RealEstateCard extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 2), child: Row( children: [ - Icon( - icon, - size: 16 + appState.getTextSizeOffset(), - color: theme.primaryColor, - ), - const SizedBox(width: 4), - isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + Icon( + icon, + size: 16 + appState.getTextSizeOffset(), + color: theme.primaryColor, + ), + const SizedBox(width: 4), + isLoading + ? ShimmerUtils.originalColorShimmer( + child: Text( + value ?? '', + style: TextStyle( + fontSize: 14 + appState.getTextSizeOffset(), + fontWeight: FontWeight.bold, + color: theme.textTheme.bodyLarge?.color, + height: 1.1, + ), + ), + color: theme.textTheme.bodyLarge?.color, + ) + : Text( value ?? '', style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), @@ -286,19 +314,8 @@ class RealEstateCard extends StatelessWidget { height: 1.1, ), ), - color: theme.textTheme.bodyLarge?.color, - ) - : Text( - value ?? '', - style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), - fontWeight: FontWeight.bold, - color: theme.textTheme.bodyLarge?.color, - height: 1.1, - ), - ), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/dashboard/widgets/rents_card.dart b/lib/pages/dashboard/widgets/rents_card.dart index 87a830c..fa417e3 100644 --- a/lib/pages/dashboard/widgets/rents_card.dart +++ b/lib/pages/dashboard/widgets/rents_card.dart @@ -13,7 +13,8 @@ class RentsCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const RentsCard({super.key, required this.showAmounts, required this.isLoading}); + const RentsCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -26,7 +27,11 @@ class RentsCard extends StatelessWidget { Icons.attach_money, Row( children: [ - UIUtils.buildValueBeforeText(context, '${dataManager.netGlobalApy.toStringAsFixed(2)}%' as String?, S.of(context).annualYield, dataManager.isLoadingMain), + UIUtils.buildValueBeforeText( + context, + '${dataManager.netGlobalApy.toStringAsFixed(2)}%' as String?, + S.of(context).annualYield, + dataManager.isLoadingMain), SizedBox(width: 6), GestureDetector( onTap: () { @@ -37,7 +42,8 @@ class RentsCard extends StatelessWidget { title: Text(S.of(context).apy), content: Text( S.of(context).netApyHelp, - style: TextStyle(fontSize: 13 + appState.getTextSizeOffset()), + style: TextStyle( + fontSize: 13 + appState.getTextSizeOffset()), ), actions: [ TextButton( @@ -57,7 +63,9 @@ class RentsCard extends StatelessWidget { Text( '(${dataManager.averageAnnualYield.toStringAsFixed(2)}% brut)', style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Colors.grey[600], ), ), @@ -65,13 +73,19 @@ class RentsCard extends StatelessWidget { ), [ UIUtils.buildTextWithShimmer( - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.dailyRent), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager.dailyRent), + currencyUtils.currencySymbol, + showAmounts), S.of(context).daily, isLoading || dataManager.isLoadingMain, context, ), UIUtils.buildTextWithShimmer( - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.weeklyRent), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager.weeklyRent), + currencyUtils.currencySymbol, + showAmounts), S.of(context).weekly, isLoading || dataManager.isLoadingMain, context, @@ -87,7 +101,10 @@ class RentsCard extends StatelessWidget { context, ), UIUtils.buildTextWithShimmer( - currencyUtils.getFormattedAmount(currencyUtils.convert(dataManager.yearlyRent), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(dataManager.yearlyRent), + currencyUtils.currencySymbol, + showAmounts), S.of(context).annually, isLoading || dataManager.isLoadingMain, context, @@ -96,8 +113,9 @@ class RentsCard extends StatelessWidget { dataManager, context, hasGraph: true, - rightWidget: _buildMiniGraphForRendement(_getLast12WeeksRent(dataManager), context, dataManager), - headerRightWidget: Container( + rightWidget: _buildMiniGraphForRendement( + _getLast12WeeksRent(dataManager), context, dataManager), + headerRightWidget: SizedBox( height: 36, width: 36, child: Material( @@ -110,7 +128,9 @@ class RentsCard extends StatelessWidget { child: Icon( Icons.arrow_forward_ios_rounded, size: 16, - color: Theme.of(context).brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), onTap: () { @@ -127,16 +147,22 @@ class RentsCard extends StatelessWidget { ); } - Widget _buildMiniGraphForRendement(List> data, BuildContext context, DataManager dataManager) { + Widget _buildMiniGraphForRendement(List> data, + BuildContext context, DataManager dataManager) { final currencyUtils = Provider.of(context, listen: false); // Calculer les valeurs min et max des données après conversion List convertedValues = List.generate(data.length, (index) { - return double.parse(currencyUtils.convert(data[index]['amount']).toStringAsFixed(2)); + return double.parse( + currencyUtils.convert(data[index]['amount']).toStringAsFixed(2)); }); - double minY = convertedValues.isEmpty ? 0 : convertedValues.reduce((a, b) => a < b ? a : b); - double maxY = convertedValues.isEmpty ? 0 : convertedValues.reduce((a, b) => a > b ? a : b); + double minY = convertedValues.isEmpty + ? 0 + : convertedValues.reduce((a, b) => a < b ? a : b); + double maxY = convertedValues.isEmpty + ? 0 + : convertedValues.reduce((a, b) => a > b ? a : b); // Ajouter une petite marge en bas et en haut minY = minY * 0.97; // 5% de marge en bas @@ -165,7 +191,8 @@ class RentsCard extends StatelessWidget { color: Theme.of(context).primaryColor, dotData: FlDotData( show: true, - getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter( + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( radius: 2, color: Theme.of(context).primaryColor, strokeWidth: 0, @@ -175,8 +202,8 @@ class RentsCard extends StatelessWidget { show: true, gradient: LinearGradient( colors: [ - Theme.of(context).primaryColor.withOpacity(0.4), - Theme.of(context).primaryColor.withOpacity(0), + Theme.of(context).primaryColor.withValues(alpha: 0.4), + Theme.of(context).primaryColor.withValues(alpha: 0), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -213,7 +240,8 @@ class RentsCard extends StatelessWidget { final currentDate = DateTime.now(); final rentData = dataManager.rentData; - DateTime currentMonday = currentDate.subtract(Duration(days: currentDate.weekday - 1)); + DateTime currentMonday = + currentDate.subtract(Duration(days: currentDate.weekday - 1)); Map weeklyRent = {}; @@ -230,10 +258,14 @@ class RentsCard extends StatelessWidget { } List> last12WeeksData = List.generate(12, (index) { - DateTime pastMonday = currentMonday.subtract(Duration(days: (index + 1) * 7)); + DateTime pastMonday = + currentMonday.subtract(Duration(days: (index + 1) * 7)); String weekLabel = DateFormat('dd MMM yyyy').format(pastMonday); - return {'week': weekLabel, 'amount': weeklyRent[DateFormat('yyyy-MM-dd').format(pastMonday)] ?? 0}; + return { + 'week': weekLabel, + 'amount': weeklyRent[DateFormat('yyyy-MM-dd').format(pastMonday)] ?? 0 + }; }).reversed.toList(); return last12WeeksData; diff --git a/lib/pages/dashboard/widgets/rmm_card.dart b/lib/pages/dashboard/widgets/rmm_card.dart index 3ee77d3..8af39c4 100644 --- a/lib/pages/dashboard/widgets/rmm_card.dart +++ b/lib/pages/dashboard/widgets/rmm_card.dart @@ -7,14 +7,14 @@ import 'package:realtoken_asset_tracker/utils/widget_factory.dart'; import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/rmm_details_page.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; class RmmCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const RmmCard({Key? key, required this.showAmounts, required this.isLoading}) : super(key: key); + const RmmCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { @@ -24,7 +24,8 @@ class RmmCard extends StatelessWidget { final theme = Theme.of(context); // Récupérer la liste des wallets depuis perWalletBalances - final List> walletDetails = dataManager.perWalletBalances; + final List> walletDetails = + dataManager.perWalletBalances; // Calcul des montants globaux (somme de tous les wallets) double totalUsdcDeposit = 0; @@ -54,10 +55,13 @@ class RmmCard extends StatelessWidget { final double totalBorrow = usdcBorrow + xdaiBorrow; // Utilise la même valeur RMM que dans l'autre page - final double walletRmmValue = dataManager.perWalletRmmValues[address] ?? 0; + final double walletRmmValue = + dataManager.perWalletRmmValues[address] ?? 0; // Calcul du Health Factor basé sur walletRmmValue - final double hf = totalBorrow > 0 ? (walletRmmValue * 0.7) / totalBorrow : double.infinity; + final double hf = totalBorrow > 0 + ? (walletRmmValue * 0.7) / totalBorrow + : double.infinity; if (hf < lowestHF) { lowestHF = hf; @@ -67,25 +71,35 @@ class RmmCard extends StatelessWidget { // Récupération des valeurs du wallet avec le HF le plus bas pour l'affichage des jauges final String selectedAddress = walletWithLowestHF?['address'] ?? ''; - final double worstWalletUsdcBorrow = walletWithLowestHF?['usdcBorrow'] as double? ?? 0; - final double worstWalletXdaiBorrow = walletWithLowestHF?['xdaiBorrow'] as double? ?? 0; - final double worstWalletBorrow = worstWalletUsdcBorrow + worstWalletXdaiBorrow; + final double worstWalletUsdcBorrow = + walletWithLowestHF?['usdcBorrow'] as double? ?? 0; + final double worstWalletXdaiBorrow = + walletWithLowestHF?['xdaiBorrow'] as double? ?? 0; + final double worstWalletBorrow = + worstWalletUsdcBorrow + worstWalletXdaiBorrow; // Récupération correcte du walletRmmValue pour le wallet avec le HF le plus bas - final double worstWalletRmmValue = dataManager.perWalletRmmValues[selectedAddress] ?? 0; + final double worstWalletRmmValue = + dataManager.perWalletRmmValues[selectedAddress] ?? 0; // Récupération des taux APY - final double usdcDepositApy = dataManager.usdcDepositApy ?? 0.0; - final double xdaiDepositApy = dataManager.xdaiDepositApy ?? 0.0; - final double usdcBorrowApy = dataManager.usdcBorrowApy ?? 0.0; - final double xdaiBorrowApy = dataManager.xdaiBorrowApy ?? 0.0; + final double usdcDepositApy = dataManager.usdcDepositApy; + final double xdaiDepositApy = dataManager.xdaiDepositApy; + final double usdcBorrowApy = dataManager.usdcBorrowApy; + final double xdaiBorrowApy = dataManager.xdaiBorrowApy; // Calcul final du Health Factor et du LTV pour le wallet le plus défavorable - double healthFactor = worstWalletBorrow > 0 ? (worstWalletRmmValue * 0.7) / worstWalletBorrow : double.infinity; - double currentLTV = worstWalletRmmValue > 0 ? (worstWalletBorrow / worstWalletRmmValue * 100) : 0; + double healthFactor = worstWalletBorrow > 0 + ? (worstWalletRmmValue * 0.7) / worstWalletBorrow + : double.infinity; + double currentLTV = worstWalletRmmValue > 0 + ? (worstWalletBorrow / worstWalletRmmValue * 100) + : 0; // Gestion des cas particuliers pour l'affichage - if (healthFactor.isInfinite || healthFactor.isNaN || worstWalletBorrow == 0) { + if (healthFactor.isInfinite || + healthFactor.isNaN || + worstWalletBorrow == 0) { healthFactor = 5.0; // Valeur par défaut pour un HF sûr } @@ -99,19 +113,22 @@ class RmmCard extends StatelessWidget { worstWalletRmmValue, worstWalletUsdcBorrow, worstWalletXdaiBorrow, - dataManager.usdcBorrowApy ?? 0.0, - dataManager.xdaiBorrowApy ?? 0.0, + dataManager.usdcBorrowApy, + dataManager.xdaiBorrowApy, isLoading, ), [ const SizedBox(height: 4), // Section Dépôts avec titre - WidgetFactory.buildSectionHeader(context, S.of(context).depositBalance), + WidgetFactory.buildSectionHeader(context, S.of(context).depositBalance), _buildBalanceRowWithIcon( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(totalXdaiDeposit), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalXdaiDeposit), + currencyUtils.currencySymbol, + showAmounts), S.of(context).depositBalance, 'xdai', // Type pour l'icône xdaiDepositApy, @@ -121,7 +138,10 @@ class RmmCard extends StatelessWidget { _buildBalanceRowWithIcon( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(totalUsdcDeposit), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalUsdcDeposit), + currencyUtils.currencySymbol, + showAmounts), S.of(context).depositBalance, 'usdc', // Type pour l'icône usdcDepositApy, @@ -132,11 +152,14 @@ class RmmCard extends StatelessWidget { const SizedBox(height: 6), // Section Emprunts avec titre - WidgetFactory.buildSectionHeader(context, S.of(context).borrowBalance), + WidgetFactory.buildSectionHeader(context, S.of(context).borrowBalance), _buildBalanceRowWithIcon( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(totalUsdcBorrow), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalUsdcBorrow), + currencyUtils.currencySymbol, + showAmounts), S.of(context).borrowBalance, 'usdc', // Type pour l'icône usdcBorrowApy, @@ -146,7 +169,10 @@ class RmmCard extends StatelessWidget { _buildBalanceRowWithIcon( context, - currencyUtils.getFormattedAmount(currencyUtils.convert(totalXdaiBorrow), currencyUtils.currencySymbol, showAmounts), + currencyUtils.getFormattedAmount( + currencyUtils.convert(totalXdaiBorrow), + currencyUtils.currencySymbol, + showAmounts), S.of(context).borrowBalance, 'xdai', // Type pour l'icône xdaiBorrowApy, @@ -158,7 +184,7 @@ class RmmCard extends StatelessWidget { context, hasGraph: true, // Flèche de navigation dans l'en-tête - headerRightWidget: Container( + headerRightWidget: SizedBox( height: 36, width: 36, child: Material( @@ -171,7 +197,9 @@ class RmmCard extends StatelessWidget { child: Icon( Icons.arrow_forward_ios_rounded, size: 16, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), onTap: () { @@ -193,7 +221,8 @@ class RmmCard extends StatelessWidget { builder: (context) { double factor = healthFactor; factor = factor.isNaN || factor < 0 ? 0 : factor.clamp(0.0, 5.0); - return _buildVerticalGauges(factor, worstWalletRmmValue, worstWalletBorrow, context); + return _buildVerticalGauges( + factor, worstWalletRmmValue, worstWalletBorrow, context); }, ), ], @@ -225,11 +254,15 @@ class RmmCard extends StatelessWidget { height: 22, decoration: BoxDecoration( shape: BoxShape.circle, - color: tokenType.toLowerCase() == 'usdc' ? Color(0xFF2775CA).withOpacity(0.15) : Color(0xFFEDB047).withOpacity(0.15), + color: tokenType.toLowerCase() == 'usdc' + ? Color(0xFF2775CA).withValues(alpha: 0.15) + : Color(0xFFEDB047).withValues(alpha: 0.15), ), child: Center( child: Image.asset( - tokenType.toLowerCase() == 'usdc' ? 'assets/icons/usdc.png' : 'assets/icons/xdai.png', + tokenType.toLowerCase() == 'usdc' + ? 'assets/icons/usdc.png' + : 'assets/icons/xdai.png', width: 16, height: 16, ), @@ -248,7 +281,9 @@ class RmmCard extends StatelessWidget { child: Text( amount, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, color: theme.textTheme.bodyLarge?.color, @@ -259,7 +294,9 @@ class RmmCard extends StatelessWidget { : Text( amount, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, letterSpacing: -0.3, color: theme.textTheme.bodyLarge?.color, @@ -271,17 +308,25 @@ class RmmCard extends StatelessWidget { isLoading ? ShimmerUtils.originalColorShimmer( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: (apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30)).withOpacity(0.15), + color: (apy >= 0 + ? Color(0xFF34C759) + : Color(0xFFFF3B30)) + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( '${apy.toStringAsFixed(1)}%', style: TextStyle( - color: apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30), + color: apy >= 0 + ? Color(0xFF34C759) + : Color(0xFFFF3B30), fontWeight: FontWeight.w500, - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.3, ), ), @@ -289,17 +334,24 @@ class RmmCard extends StatelessWidget { color: apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30), ) : Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: (apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30)).withOpacity(0.15), + color: + (apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30)) + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( '${apy.toStringAsFixed(1)}%', style: TextStyle( - color: apy >= 0 ? Color(0xFF34C759) : Color(0xFFFF3B30), + color: apy >= 0 + ? Color(0xFF34C759) + : Color(0xFFFF3B30), fontWeight: FontWeight.w500, - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.3, ), ), @@ -312,7 +364,8 @@ class RmmCard extends StatelessWidget { ); } - Widget _buildVerticalGauges(double factor, double walletDeposit, double walletBorrow, BuildContext context) { + Widget _buildVerticalGauges(double factor, double walletDeposit, + double walletBorrow, BuildContext context) { // Obtenir le wallet avec le HF le plus bas depuis le contexte final dataManager = Provider.of(context, listen: false); final theme = Theme.of(context); @@ -330,7 +383,9 @@ class RmmCard extends StatelessWidget { final walletRmmValue = dataManager.perWalletRmmValues[address] ?? 0; - final hf = totalBorrow > 0 ? (walletRmmValue * 0.7) / totalBorrow : double.infinity; + final hf = totalBorrow > 0 + ? (walletRmmValue * 0.7) / totalBorrow + : double.infinity; if (hf < lowestHF) { lowestHF = hf; @@ -339,15 +394,20 @@ class RmmCard extends StatelessWidget { } // Adresse abrégée pour affichage (6 premiers et 4 derniers caractères) - String shortAddress = walletAddress.length > 10 ? "${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}" : walletAddress; + String shortAddress = walletAddress.length > 10 + ? "${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}" + : walletAddress; double progressHF = (factor / 5).clamp(0.0, 1.0); - double progressLTV = walletDeposit > 0 ? ((walletBorrow / walletDeposit * 100).clamp(0.0, 100.0)) / 100 : 0; + double progressLTV = walletDeposit > 0 + ? ((walletBorrow / walletDeposit * 100).clamp(0.0, 100.0)) / 100 + : 0; // Définition des couleurs pour la jauge HF en fonction du facteur Color getHFColor(double hfValue) { if (hfValue <= 1.1) { - return Color(0xFFFF3B30); // Rouge pour valeurs dangereuses (HF proche de 1) + return Color( + 0xFFFF3B30); // Rouge pour valeurs dangereuses (HF proche de 1) } else if (hfValue <= 1.5) { return Color(0xFFFF9500); // Orange pour valeurs à risque modéré } else if (hfValue <= 2.5) { @@ -360,7 +420,8 @@ class RmmCard extends StatelessWidget { // Fonction pour déterminer la couleur de la jauge LTV en fonction de sa valeur Color getLTVColor(double ltvPercent) { if (ltvPercent >= 65) { - return Color(0xFFFF3B30); // Rouge pour valeurs dangereuses (LTV proche de 70%) + return Color( + 0xFFFF3B30); // Rouge pour valeurs dangereuses (LTV proche de 70%) } else if (ltvPercent >= 55) { return Color(0xFFFF9500); // Orange pour valeurs à risque modéré } else if (ltvPercent >= 40) { @@ -394,7 +455,9 @@ class RmmCard extends StatelessWidget { Text( 'HF', style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, @@ -406,7 +469,9 @@ class RmmCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.05) : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), ), child: Stack( children: [ @@ -417,8 +482,12 @@ class RmmCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: progressHF > 0.95 ? Radius.circular(13) : Radius.zero, - topRight: progressHF > 0.95 ? Radius.circular(13) : Radius.zero, + topLeft: progressHF > 0.95 + ? Radius.circular(13) + : Radius.zero, + topRight: progressHF > 0.95 + ? Radius.circular(13) + : Radius.zero, bottomLeft: Radius.circular(13), bottomRight: Radius.circular(13), ), @@ -428,17 +497,20 @@ class RmmCard extends StatelessWidget { ), // Ligne de seuil critique pour HF à 1 Positioned( - bottom: (1 / 5) * 80, // La position 1 sur l'échelle 0-5 + bottom: + (1 / 5) * 80, // La position 1 sur l'échelle 0-5 left: -2, right: -2, child: Container( height: 1.5, decoration: BoxDecoration( - color: Color(0xFFFF3B30), // Rouge pour la ligne critique + color: Color( + 0xFFFF3B30), // Rouge pour la ligne critique borderRadius: BorderRadius.circular(1), boxShadow: [ BoxShadow( - color: Color(0xFFFF3B30).withOpacity(0.5), + color: + Color(0xFFFF3B30).withValues(alpha: 0.5), blurRadius: 2, spreadRadius: 0, ), @@ -453,13 +525,15 @@ class RmmCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: hfGaugeColor.withOpacity(0.15), + color: hfGaugeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( (progressHF * 5).toStringAsFixed(1), style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: hfGaugeColor, ), @@ -474,7 +548,9 @@ class RmmCard extends StatelessWidget { Text( 'LTV', style: TextStyle( - fontSize: 15 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, letterSpacing: -0.3, color: theme.textTheme.bodyMedium?.color, @@ -486,7 +562,9 @@ class RmmCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.05) : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), ), child: Stack( children: [ @@ -498,7 +576,8 @@ class RmmCard extends StatelessWidget { child: Container( height: 1.5, decoration: BoxDecoration( - color: Color(0xFFFF3B30), // Rouge pour la ligne critique + color: Color( + 0xFFFF3B30), // Rouge pour la ligne critique borderRadius: BorderRadius.circular(1), ), ), @@ -510,8 +589,12 @@ class RmmCard extends StatelessWidget { width: 20, decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: progressLTV > 0.95 ? Radius.circular(13) : Radius.zero, - topRight: progressLTV > 0.95 ? Radius.circular(13) : Radius.zero, + topLeft: progressLTV > 0.95 + ? Radius.circular(13) + : Radius.zero, + topRight: progressLTV > 0.95 + ? Radius.circular(13) + : Radius.zero, bottomLeft: Radius.circular(13), bottomRight: Radius.circular(13), ), @@ -526,13 +609,15 @@ class RmmCard extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: ltvGaugeColor.withOpacity(0.15), + color: ltvGaugeColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text( '${(progressLTV * 100).toStringAsFixed(0)}%', style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: ltvGaugeColor, ), @@ -550,15 +635,21 @@ class RmmCard extends StatelessWidget { child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: theme.brightness == Brightness.light ? Colors.black.withOpacity(0.05) : Colors.white.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Text( shortAddress, style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), letterSpacing: -0.3, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), overflow: TextOverflow.ellipsis, ), @@ -583,9 +674,11 @@ class RmmCard extends StatelessWidget { if (isLoading) { return ShimmerUtils.originalColorShimmer( child: Text( - "${S.of(context).timeBeforeLiquidation}", + S.of(context).timeBeforeLiquidation, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, color: theme.textTheme.bodyMedium?.color, letterSpacing: -0.3, @@ -621,15 +714,15 @@ class RmmCard extends StatelessWidget { if (timeStatus == "danger") { iconData = Icons.error_rounded; iconColor = Color(0xFFFF3B30); - bgColor = Color(0xFFFF3B30).withOpacity(0.15); + bgColor = Color(0xFFFF3B30).withValues(alpha: 0.15); } else if (timeStatus == "warning") { iconData = Icons.warning_rounded; iconColor = Color(0xFFFF9500); - bgColor = Color(0xFFFF9500).withOpacity(0.15); + bgColor = Color(0xFFFF9500).withValues(alpha: 0.15); } else { iconData = Icons.check_circle_rounded; iconColor = Color(0xFF34C759); - bgColor = Color(0xFF34C759).withOpacity(0.15); + bgColor = Color(0xFF34C759).withValues(alpha: 0.15); } return Container( @@ -651,7 +744,9 @@ class RmmCard extends StatelessWidget { child: Text( "${realTime.isNotEmpty ? "$realTime " : ""}${S.of(context).timeBeforeLiquidation}", style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, color: theme.textTheme.bodyMedium?.color, letterSpacing: -0.3, @@ -664,8 +759,6 @@ class RmmCard extends StatelessWidget { ); } - - // Déterminer le statut de temps avant liquidation String _calculateTimeBeforeLiquidationStatus( double walletRmmValue, @@ -740,10 +833,12 @@ class RmmCard extends StatelessWidget { double xdaiBorrowApy, ) { // Récupérer les valeurs depuis le widget parent - final DataManager dataManager = Provider.of(context, listen: false); + final DataManager dataManager = + Provider.of(context, listen: false); // Récupérer la liste des wallets depuis perWalletBalances - final List> walletDetails = dataManager.perWalletBalances; + final List> walletDetails = + dataManager.perWalletBalances; // Sélection du wallet ayant le HF le plus bas Map? walletWithLowestHF; @@ -756,10 +851,13 @@ class RmmCard extends StatelessWidget { final double totalBorrow = usdcBorrow + xdaiBorrow; // Utilise la même valeur RMM que dans l'autre page - final double walletRmmValue = dataManager.perWalletRmmValues[address] ?? 0; + final double walletRmmValue = + dataManager.perWalletRmmValues[address] ?? 0; // Calcul du Health Factor basé sur walletRmmValue - final double hf = totalBorrow > 0 ? (walletRmmValue * 0.7) / totalBorrow : double.infinity; + final double hf = totalBorrow > 0 + ? (walletRmmValue * 0.7) / totalBorrow + : double.infinity; if (hf < lowestHF) { lowestHF = hf; @@ -772,7 +870,8 @@ class RmmCard extends StatelessWidget { final double usdcBorrow = walletWithLowestHF?['usdcBorrow'] as double? ?? 0; final double xdaiBorrow = walletWithLowestHF?['xdaiBorrow'] as double? ?? 0; final double totalBorrow = usdcBorrow + xdaiBorrow; - final double walletRmmValue = dataManager.perWalletRmmValues[selectedAddress] ?? 0; + final double walletRmmValue = + dataManager.perWalletRmmValues[selectedAddress] ?? 0; // Si pas d'emprunt, pas de risque de liquidation if (totalBorrow == 0) { @@ -788,8 +887,8 @@ class RmmCard extends StatelessWidget { } // Taux d'emprunt - final double usdcBorrowApy = dataManager.usdcBorrowApy ?? 0.0; - final double xdaiBorrowApy = dataManager.xdaiBorrowApy ?? 0.0; + final double usdcBorrowApy = dataManager.usdcBorrowApy; + final double xdaiBorrowApy = dataManager.xdaiBorrowApy; // Calcul des taux d'intérêt journaliers final double usdcDailyRate = usdcBorrowApy / 365 / 100; diff --git a/lib/pages/dashboard/widgets/tokens_card.dart b/lib/pages/dashboard/widgets/tokens_card.dart index bb18c1c..03e53ea 100644 --- a/lib/pages/dashboard/widgets/tokens_card.dart +++ b/lib/pages/dashboard/widgets/tokens_card.dart @@ -10,22 +10,36 @@ class TokensCard extends StatelessWidget { final bool showAmounts; final bool isLoading; - const TokensCard({super.key, required this.showAmounts, required this.isLoading}); + const TokensCard( + {super.key, required this.showAmounts, required this.isLoading}); @override Widget build(BuildContext context) { final dataManager = Provider.of(context); // Calculer la répartition par productType (somme des quantités) - final realEstateCount = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'real_estate_rental' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - final loanCount = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'loan_income' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - final factoringCount = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'factoring_profitshare' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + final realEstateCount = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'real_estate_rental') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + final loanCount = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'loan_income') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + final factoringCount = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'factoring_profitshare') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); final totalCount = realEstateCount + loanCount + factoringCount; return Stack( @@ -33,7 +47,11 @@ class TokensCard extends StatelessWidget { UIUtils.buildCard( S.of(context).tokens, Icons.token_outlined, - UIUtils.buildValueBeforeText(context, dataManager.totalTokens.toStringAsFixed(2) as String?, S.of(context).totalTokens, isLoading || dataManager.isLoadingMain), + UIUtils.buildValueBeforeText( + context, + dataManager.totalTokens.toStringAsFixed(2) as String?, + S.of(context).totalTokens, + isLoading || dataManager.isLoadingMain), [ _buildTokensTable(context, dataManager), ], @@ -44,13 +62,15 @@ class TokensCard extends StatelessWidget { Positioned( top: 30, // Position négative pour remonter le donut right: 12, // Alignement à droite - child: _buildPieChart(realEstateCount, loanCount, factoringCount, totalCount, context), + child: _buildPieChart( + realEstateCount, loanCount, factoringCount, totalCount, context), ), ], ); } - Widget _buildPieChart(double realEstateCount, double loanCount, double factoringCount, double totalCount, BuildContext context) { + Widget _buildPieChart(double realEstateCount, double loanCount, + double factoringCount, double totalCount, BuildContext context) { // Si pas de tokens, afficher un graphique vide if (totalCount == 0) { return SizedBox( @@ -67,7 +87,7 @@ class TokensCard extends StatelessWidget { } List sections = []; - + // Real Estate (vert) if (realEstateCount > 0) { sections.add(PieChartSectionData( @@ -82,7 +102,7 @@ class TokensCard extends StatelessWidget { ), )); } - + // Loan Income (bleu - couleur du thème) if (loanCount > 0) { sections.add(PieChartSectionData( @@ -92,7 +112,7 @@ class TokensCard extends StatelessWidget { radius: 23, gradient: LinearGradient( colors: [ - Theme.of(context).primaryColor.withOpacity(0.7), + Theme.of(context).primaryColor.withValues(alpha: 0.7), Theme.of(context).primaryColor, ], begin: Alignment.topLeft, @@ -100,7 +120,7 @@ class TokensCard extends StatelessWidget { ), )); } - + // Factoring (orange) if (factoringCount > 0) { sections.add(PieChartSectionData( @@ -135,44 +155,73 @@ class TokensCard extends StatelessWidget { Widget _buildTokensTable(BuildContext context, DataManager dataManager) { // Calculer la somme des quantités par productType et source - final walletRealEstate = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'real_estate_rental' && - token['source'] == 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - - final walletLoan = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'loan_income' && - token['source'] == 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - - final walletFactoring = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'factoring_profitshare' && - token['source'] == 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - - final rmmRealEstate = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'real_estate_rental' && - token['source'] != 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - - final rmmLoan = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'loan_income' && - token['source'] != 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); - - final rmmFactoring = dataManager.portfolio.where((token) => - (token['productType'] ?? '').toLowerCase() == 'factoring_profitshare' && - token['source'] != 'wallet' - ).fold(0.0, (sum, token) => sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + final walletRealEstate = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'real_estate_rental' && + token['source'] == 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + + final walletLoan = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'loan_income' && + token['source'] == 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + + final walletFactoring = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'factoring_profitshare' && + token['source'] == 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + + final rmmRealEstate = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'real_estate_rental' && + token['source'] != 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + + final rmmLoan = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == 'loan_income' && + token['source'] != 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); + + final rmmFactoring = dataManager.portfolio + .where((token) => + (token['productType'] ?? '').toLowerCase() == + 'factoring_profitshare' && + token['source'] != 'wallet') + .fold( + 0.0, + (sum, token) => + sum + ((token['amount'] as num?)?.toDouble() ?? 0.0)); return Container( width: double.infinity, - margin: const EdgeInsets.only(right: 130), // Laisser de l'espace pour le donut (120px + 10px marge) + margin: const EdgeInsets.only( + right: 130), // Laisser de l'espace pour le donut (120px + 10px marge) child: Table( columnWidths: const { 0: FixedColumnWidth(30), // Colonne icône - 1: FlexColumnWidth(1), // Colonne Wallet - 2: FlexColumnWidth(1), // Colonne RMM + 1: FlexColumnWidth(1), // Colonne Wallet + 2: FlexColumnWidth(1), // Colonne RMM }, children: [ // Ligne d'en-tête @@ -186,15 +235,19 @@ class TokensCard extends StatelessWidget { // Ligne Real Estate TableRow( children: [ - _buildIconCell(Icons.home_outlined, _getRealEstateTableColor(), context), - _buildValueCell(walletRealEstate, _getRealEstateTableColor(), context), - _buildValueCell(rmmRealEstate, _getRealEstateTableColor(), context), + _buildIconCell( + Icons.home_outlined, _getRealEstateTableColor(), context), + _buildValueCell( + walletRealEstate, _getRealEstateTableColor(), context), + _buildValueCell( + rmmRealEstate, _getRealEstateTableColor(), context), ], ), // Ligne Loan TableRow( children: [ - _buildIconCell(Icons.account_balance_outlined, _getLoanTableColor(), context), + _buildIconCell(Icons.account_balance_outlined, + _getLoanTableColor(), context), _buildValueCell(walletLoan, _getLoanTableColor(), context), _buildValueCell(rmmLoan, _getLoanTableColor(), context), ], @@ -202,8 +255,10 @@ class TokensCard extends StatelessWidget { // Ligne Factoring TableRow( children: [ - _buildIconCell(Icons.business_center_outlined, _getFactoringTableColor(), context), - _buildValueCell(walletFactoring, _getFactoringTableColor(), context), + _buildIconCell(Icons.business_center_outlined, + _getFactoringTableColor(), context), + _buildValueCell( + walletFactoring, _getFactoringTableColor(), context), _buildValueCell(rmmFactoring, _getFactoringTableColor(), context), ], ), @@ -221,10 +276,12 @@ class TokensCard extends StatelessWidget { Text( label, style: TextStyle( - fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), - color: Theme.of(context).brightness == Brightness.light - ? Colors.black54 - : Colors.white70, + fontSize: 11 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + color: Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : Colors.white70, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, @@ -233,7 +290,9 @@ class TokensCard extends StatelessWidget { Text( value, style: TextStyle( - fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color, ), @@ -250,10 +309,11 @@ class TokensCard extends StatelessWidget { child: Text( label, style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), - color: Theme.of(context).brightness == Brightness.light - ? Colors.black87 - : Colors.white, + fontSize: 12 + + Provider.of(context, listen: false).getTextSizeOffset(), + color: Theme.of(context).brightness == Brightness.light + ? Colors.black87 + : Colors.white, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, @@ -261,7 +321,8 @@ class TokensCard extends StatelessWidget { ); } - Widget _buildDetailCell(String emoji, int count, Color color, BuildContext context) { + Widget _buildDetailCell( + String emoji, int count, Color color, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 4), child: Row( @@ -269,13 +330,18 @@ class TokensCard extends StatelessWidget { children: [ Text( emoji, - style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset()), + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset()), ), const SizedBox(width: 4), Text( '$count', style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: color, ), @@ -304,7 +370,8 @@ class TokensCard extends StatelessWidget { child: Text( count.toStringAsFixed(2), style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600, color: color, ), diff --git a/lib/pages/links_page.dart b/lib/pages/links_page.dart index a0a9611..59047f5 100644 --- a/lib/pages/links_page.dart +++ b/lib/pages/links_page.dart @@ -39,7 +39,8 @@ class RealtPageState extends State { body: SafeArea( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,7 +93,7 @@ class RealtPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 5, offset: const Offset(0, 2), ), @@ -126,17 +127,23 @@ class RealtPageState extends State { Text( linkText, style: TextStyle( - fontSize: 16 + Provider.of(context).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context) + .getTextSizeOffset(), fontWeight: FontWeight.w500, - color: CupertinoColors.systemBlue.resolveFrom(context), + color: CupertinoColors.systemBlue + .resolveFrom(context), ), ), const SizedBox(height: 6), Text( description, style: TextStyle( - fontSize: 14 + Provider.of(context).getTextSizeOffset(), - color: CupertinoColors.secondaryLabel.resolveFrom(context), + fontSize: 14 + + Provider.of(context) + .getTextSizeOffset(), + color: CupertinoColors.secondaryLabel + .resolveFrom(context), ), ), ], diff --git a/lib/pages/maps_page.dart b/lib/pages/maps_page.dart index 28837f6..37a3344 100644 --- a/lib/pages/maps_page.dart +++ b/lib/pages/maps_page.dart @@ -27,7 +27,8 @@ class MapsPage extends StatefulWidget { const MapsPage({super.key}); @override - MapsPageState createState() => MapsPageState(); // Remplacer _MapsPageState par MapsPageState + MapsPageState createState() => + MapsPageState(); // Remplacer _MapsPageState par MapsPageState } class MapsPageState extends State { @@ -38,37 +39,37 @@ class MapsPageState extends State { final String _sortOption = 'Name'; final bool _isAscending = true; bool _forceLightMode = false; - + // Nouveaux filtres avancés - bool _showHeatmapRent = false; - bool _showHeatmapPerformance = false; - bool _showYamOffers = false; - bool _showRecentTransactions = false; + final bool _showHeatmapRent = false; + final bool _showHeatmapPerformance = false; + final bool _showYamOffers = false; + final bool _showRecentTransactions = false; bool _showMiniDashboard = false; - + // Filtres de rentabilité double _minApy = 0.0; double _maxApy = 50.0; bool _onlyWithRent = false; bool _onlyFullyRented = false; - + // Filtres par région String? _selectedCountry; - List _availableCountries = []; - + final List _availableCountries = []; + // Filtres par performance - double _minRoi = -100.0; // Permettre les ROI négatifs + double _minRoi = -100.0; // Permettre les ROI négatifs double _maxRoi = 100.0; - + // Contrôleur pour le panneau de filtres bool _showFiltersPanel = false; final MapController _mapController = MapController(); - + // Variables pour l'atténuation du mini-dashboard pendant les interactions double _dashboardOpacity = 1.0; Timer? _dashboardTimer; - + // Mode de coloration des markers et clusters ColorationMode _colorationMode = ColorationMode.apy; @@ -92,14 +93,16 @@ class MapsPageState extends State { Future _loadThemePreference() async { SharedPreferences prefs = await SharedPreferences.getInstance(); setState(() { - _forceLightMode = prefs.getBool('forceLightMode') ?? false; // Charger le mode forcé + _forceLightMode = + prefs.getBool('forceLightMode') ?? false; // Charger le mode forcé }); } // Sauvegarder la préférence du mode dans SharedPreferences Future _saveThemePreference() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setBool('forceLightMode', _forceLightMode); // Sauvegarder le mode forcé + await prefs.setBool( + 'forceLightMode', _forceLightMode); // Sauvegarder le mode forcé } // Charger la préférence du mode de coloration à partir de SharedPreferences @@ -107,21 +110,24 @@ class MapsPageState extends State { SharedPreferences prefs = await SharedPreferences.getInstance(); final colorationModeString = prefs.getString('colorationMode') ?? 'apy'; setState(() { - _colorationMode = colorationModeString == 'rental' ? ColorationMode.rental : ColorationMode.apy; + _colorationMode = colorationModeString == 'rental' + ? ColorationMode.rental + : ColorationMode.apy; }); } // Sauvegarder la préférence du mode de coloration dans SharedPreferences Future _saveColorationModePreference() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('colorationMode', _colorationMode == ColorationMode.rental ? 'rental' : 'apy'); + await prefs.setString('colorationMode', + _colorationMode == ColorationMode.rental ? 'rental' : 'apy'); } // Gérer l'atténuation du panneau de contrôle et fermer les panneaux ouverts void _onMapInteraction() { // Annuler le timer précédent s'il existe _dashboardTimer?.cancel(); - + // Fermer les panneaux Statistics et Filtres s'ils sont ouverts bool shouldUpdate = false; if (_showMiniDashboard) { @@ -132,18 +138,18 @@ class MapsPageState extends State { _showFiltersPanel = false; shouldUpdate = true; } - + // Atténuer immédiatement le panneau de contrôle principal if (_dashboardOpacity == 1.0) { _dashboardOpacity = 0.3; shouldUpdate = true; } - + // Appliquer les changements si nécessaire if (shouldUpdate) { setState(() {}); } - + // Programmer le retour à l'opacité normale après 2.5 secondes _dashboardTimer = Timer(const Duration(milliseconds: 2500), () { if (mounted) { @@ -158,15 +164,15 @@ class MapsPageState extends State { double _getTokenRentSafely(String tokenUuid, DataManager dataManager) { // Vérifier d'abord si c'est dans le portefeuille final portfolioToken = dataManager.portfolio.firstWhere( - (portfolioToken) => portfolioToken['uuid'].toLowerCase() == tokenUuid.toLowerCase(), - orElse: () => {} - ); - + (portfolioToken) => + portfolioToken['uuid'].toLowerCase() == tokenUuid.toLowerCase(), + orElse: () => {}); + // Si pas dans le portefeuille, pas de loyers if (portfolioToken.isEmpty) { return 0.0; } - + // Utiliser les données précalculées pour tous les tokens possédés (Wallet ET RMM) return dataManager.cumulativeRentsByToken[tokenUuid.toLowerCase()] ?? 0.0; } @@ -184,7 +190,7 @@ class MapsPageState extends State { for (var marker in clusterMarkers) { final lat = marker.point.latitude; final lng = marker.point.longitude; - + minLat = min(minLat, lat); maxLat = max(maxLat, lat); minLng = min(minLng, lng); @@ -199,8 +205,10 @@ class MapsPageState extends State { // Ajouter une marge de 20% autour des limites final margin = 0.2; - final boundsSouthWest = LatLng(minLat - latDiff * margin, minLng - lngDiff * margin); - final boundsNorthEast = LatLng(maxLat + latDiff * margin, maxLng + lngDiff * margin); + final boundsSouthWest = + LatLng(minLat - latDiff * margin, minLng - lngDiff * margin); + final boundsNorthEast = + LatLng(maxLat + latDiff * margin, maxLng + lngDiff * margin); // Zoomer sur la zone calculée _mapController.fitCamera(CameraFit.bounds( @@ -210,25 +218,28 @@ class MapsPageState extends State { } // Méthode pour filtrer et trier les tokens avec critères avancés - List> _filterAndSortTokens(List> tokens, DataManager dataManager) { + List> _filterAndSortTokens( + List> tokens, DataManager dataManager) { List> filteredTokens = tokens.where((token) { // Exclure les tokens factoring_profitshare (pas des propriétés réelles) final productType = token['productType']?.toString().toLowerCase() ?? ''; if (productType == 'factoring_profitshare') { return false; } - + // Filtre de recherche textuelle - if (!token['fullName'].toLowerCase().contains(_searchQuery.toLowerCase())) { + if (!token['fullName'] + .toLowerCase() + .contains(_searchQuery.toLowerCase())) { return false; } - + // Filtre APY final apy = token['annualPercentageYield'] ?? 0.0; if (apy < _minApy || apy > _maxApy) { return false; } - + // Filtre uniquement avec loyers if (_onlyWithRent) { final totalRent = _getTokenRentSafely(token['uuid'], dataManager); @@ -236,7 +247,7 @@ class MapsPageState extends State { return false; } } - + // Filtre uniquement entièrement loués if (_onlyFullyRented) { final rentedUnits = token['rentedUnits'] ?? 0; @@ -245,32 +256,40 @@ class MapsPageState extends State { return false; } } - + // Filtre par pays if (_selectedCountry != null && _selectedCountry!.isNotEmpty) { if (!_matchesCountryFilter(token, _selectedCountry)) { return false; } } - + // Filtre ROI (si applicable) final initialValue = token['initialTotalValue'] ?? token['tokenPrice']; final currentValue = token['tokenPrice'] ?? 0.0; - final roi = initialValue > 0 ? ((currentValue - initialValue) / initialValue * 100) : 0.0; + final roi = initialValue > 0 + ? ((currentValue - initialValue) / initialValue * 100) + : 0.0; if (roi < _minRoi || roi > _maxRoi) { return false; } - + return true; }).toList(); // Tri if (_sortOption == 'Name') { - filteredTokens.sort((a, b) => _isAscending ? a['shortName'].compareTo(b['shortName']) : b['shortName'].compareTo(a['shortName'])); + filteredTokens.sort((a, b) => _isAscending + ? a['shortName'].compareTo(b['shortName']) + : b['shortName'].compareTo(a['shortName'])); } else if (_sortOption == 'Value') { - filteredTokens.sort((a, b) => _isAscending ? a['totalValue'].compareTo(b['totalValue']) : b['totalValue'].compareTo(a['totalValue'])); + filteredTokens.sort((a, b) => _isAscending + ? a['totalValue'].compareTo(b['totalValue']) + : b['totalValue'].compareTo(a['totalValue'])); } else if (_sortOption == 'APY') { - filteredTokens.sort((a, b) => _isAscending ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); + filteredTokens.sort((a, b) => _isAscending + ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) + : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); } else if (_sortOption == 'ROI') { filteredTokens.sort((a, b) { final roiA = _calculateRoi(a); @@ -287,25 +306,29 @@ class MapsPageState extends State { return filteredTokens; } - + double _calculateRoi(Map token) { final initialValue = token['initialTotalValue'] ?? token['tokenPrice']; final currentValue = token['tokenPrice'] ?? 0.0; - return initialValue > 0 ? ((currentValue - initialValue) / initialValue * 100) : 0.0; + return initialValue > 0 + ? ((currentValue - initialValue) / initialValue * 100) + : 0.0; } - + // Méthode pour vérifier si un token correspond au filtre pays - bool _matchesCountryFilter(Map token, String? selectedCountry) { + bool _matchesCountryFilter( + Map token, String? selectedCountry) { if (selectedCountry == null) return true; - + String tokenCountry = token['country'] ?? "Unknown Country"; - + // Si "Series XX" est sélectionné, filtrer tous les tokens factoring_profitshare avec des séries if (selectedCountry == "Series XX") { - return (token['productType']?.toString().toLowerCase() == 'factoring_profitshare') && - tokenCountry.toLowerCase().startsWith('series '); + return (token['productType']?.toString().toLowerCase() == + 'factoring_profitshare') && + tokenCountry.toLowerCase().startsWith('series '); } - + // Filtre normal return tokenCountry == selectedCountry; } @@ -314,12 +337,16 @@ class MapsPageState extends State { Widget build(BuildContext context) { final dataManager = Provider.of(context); - final sourceTokens = _showAllTokens ? dataManager.allTokens : dataManager.portfolio; - + final sourceTokens = + _showAllTokens ? dataManager.allTokens : dataManager.portfolio; + final tokensToShow = _filterAndSortTokens(sourceTokens, dataManager); - final displayedTokens = _showWhitelistedTokens - ? tokensToShow.where((token) => dataManager.whitelistTokens.any((w) => w['token'].toLowerCase() == token['uuid'].toLowerCase())).toList() + final displayedTokens = _showWhitelistedTokens + ? tokensToShow + .where((token) => dataManager.whitelistTokens.any( + (w) => w['token'].toLowerCase() == token['uuid'].toLowerCase())) + .toList() : tokensToShow; final List markers = []; @@ -330,8 +357,10 @@ class MapsPageState extends State { double? lat, double? lng, }) { - final latValue = lat ?? double.tryParse(matchingToken['lat']?.toString() ?? ''); - final lngValue = lng ?? double.tryParse(matchingToken['lng']?.toString() ?? ''); + final latValue = + lat ?? double.tryParse(matchingToken['lat']?.toString() ?? ''); + final lngValue = + lng ?? double.tryParse(matchingToken['lng']?.toString() ?? ''); final rentedUnits = matchingToken['rentedUnits'] ?? 0; final totalUnits = matchingToken['totalUnits'] ?? 1; @@ -368,16 +397,17 @@ class MapsPageState extends State { // Grouper les tokens par propriété unique (même coordonnées) Map> uniqueProperties = {}; - + for (var token in displayedTokens) { // Vérification comme dans showTokenDetails.dart final double? lat = double.tryParse(token['lat']?.toString() ?? ''); final double? lng = double.tryParse(token['lng']?.toString() ?? ''); - + if (lat != null && lng != null) { // Créer une clé unique basée sur les coordonnées - final String propertyKey = '${lat.toStringAsFixed(6)}_${lng.toStringAsFixed(6)}'; - + final String propertyKey = + '${lat.toStringAsFixed(6)}_${lng.toStringAsFixed(6)}'; + if (!uniqueProperties.containsKey(propertyKey)) { // Première fois qu'on voit cette propriété uniqueProperties[propertyKey] = { @@ -392,9 +422,13 @@ class MapsPageState extends State { // Propriété déjà existante, on ajoute le token final existingProperty = uniqueProperties[propertyKey]!; (existingProperty['tokens'] as List).add(token); - existingProperty['totalAmount'] = (existingProperty['totalAmount'] as double) + (token['amount'] ?? 0.0); - existingProperty['totalValue'] = (existingProperty['totalValue'] as double) + (token['totalValue'] ?? 0.0); - + existingProperty['totalAmount'] = + (existingProperty['totalAmount'] as double) + + (token['amount'] ?? 0.0); + existingProperty['totalValue'] = + (existingProperty['totalValue'] as double) + + (token['totalValue'] ?? 0.0); + // Marquer si on a des tokens wallet ou RMM if (token['source'] == 'wallet') { existingProperty['hasWallet'] = true; @@ -409,12 +443,12 @@ class MapsPageState extends State { for (var property in uniqueProperties.values) { final lat = double.tryParse(property['lat'].toString())!; final lng = double.tryParse(property['lng'].toString())!; - + // Déterminer la couleur basée sur le type de tokens possédés Color markerColor; final hasWallet = property['hasWallet'] as bool; final hasRMM = property['hasRMM'] as bool; - + if (hasWallet && hasRMM) { markerColor = Colors.purple; // Mixte wallet + RMM } else if (hasWallet) { @@ -422,7 +456,7 @@ class MapsPageState extends State { } else { markerColor = Colors.blue; // Seulement RMM } - + markers.add( createMarker( matchingToken: property, @@ -439,7 +473,8 @@ class MapsPageState extends State { body: Stack( children: [ Container( - color: Theme.of(context).scaffoldBackgroundColor, // Définit la couleur de fond pour la carte + color: Theme.of(context) + .scaffoldBackgroundColor, // Définit la couleur de fond pour la carte child: FlutterMap( mapController: _mapController, options: MapOptions( @@ -448,7 +483,10 @@ class MapsPageState extends State { onTap: (_, __) => _popupController.hideAllPopups(), onPointerDown: (_, __) => _onMapInteraction(), onPointerHover: (_, __) => _onMapInteraction(), - interactionOptions: const InteractionOptions(flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag | InteractiveFlag.scrollWheelZoom), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.pinchZoom | + InteractiveFlag.drag | + InteractiveFlag.scrollWheelZoom), ), children: [ TileLayer( @@ -461,7 +499,8 @@ class MapsPageState extends State { subdomains: ['a', 'b', 'c'], tileProvider: kIsWeb ? NetworkTileProvider() // Utilisé uniquement pour le web - : FMTCStore('mapStore').getTileProvider(), // Utilisé pour iOS, Android, etc. + : FMTCStore('mapStore') + .getTileProvider(), // Utilisé pour iOS, Android, etc. userAgentPackageName: 'com.byackee.realtoken_asset_tracker', retinaMode: true, ), @@ -473,16 +512,19 @@ class MapsPageState extends State { size: const Size(50, 50), markers: markers, builder: (context, clusterMarkers) { - final clusterStats = _getClusterStats(clusterMarkers, dataManager); + final clusterStats = + _getClusterStats(clusterMarkers, dataManager); final Color clusterColor = clusterStats['color']; - final currencyUtils = Provider.of(context, listen: false); - + final currencyUtils = + Provider.of(context, listen: false); + return GestureDetector( onTap: () { _zoomToCluster(clusterMarkers); }, onLongPress: () { - _showClusterPopup(context, clusterStats, clusterMarkers.length, dataManager); + _showClusterPopup(context, clusterStats, + clusterMarkers.length, dataManager); }, child: Container( width: 50, @@ -491,8 +533,8 @@ class MapsPageState extends State { shape: BoxShape.circle, gradient: RadialGradient( colors: [ - clusterColor.withOpacity(0.9), - clusterColor.withOpacity(0.2), + clusterColor.withValues(alpha: 0.9), + clusterColor.withValues(alpha: 0.2), ], stops: [0.4, 1.0], ), @@ -506,7 +548,10 @@ class MapsPageState extends State { style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), Text( @@ -514,7 +559,10 @@ class MapsPageState extends State { style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 8 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 8 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ], @@ -540,12 +588,13 @@ class MapsPageState extends State { value: _forceLightMode, onChanged: (value) { setState(() { - _forceLightMode = value; // Mettre à jour le switch pour forcer le mode clair + _forceLightMode = + value; // Mettre à jour le switch pour forcer le mode clair }); _saveThemePreference(); // Sauvegarder la préférence }, - activeColor: Theme.of(context).primaryColor, - trackColor: Colors.grey.shade300, + activeTrackColor: Theme.of(context).primaryColor, + inactiveTrackColor: Colors.grey.shade300, ), ), ], @@ -559,14 +608,15 @@ class MapsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Contrôles principaux avec animation d'opacité - AnimatedOpacity( + AnimatedOpacity( opacity: _dashboardOpacity, duration: const Duration(milliseconds: 300), child: Container( width: 280, // Largeur fixe pour le panneau - padding: const EdgeInsets.fromLTRB(8, 4, 4, 4), // Padding réduit haut/bas et à droite + padding: const EdgeInsets.fromLTRB( + 8, 4, 4, 4), // Padding réduit haut/bas et à droite decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.9), + color: Theme.of(context).cardColor.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -578,7 +628,6 @@ class MapsPageState extends State { ), child: Column( children: [ - Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -598,22 +647,41 @@ class MapsPageState extends State { _showAllTokens = value; }); }, - activeColor: Theme.of(context).primaryColor, - trackColor: Colors.grey.shade300, + activeTrackColor: + Theme.of(context).primaryColor, + inactiveTrackColor: + Colors.grey.shade300, ), ), const SizedBox(width: 4), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - _showAllTokens ? S.of(context).portfolioGlobal : S.of(context).portfolio, - style: TextStyle(fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600, height: 1.1), + _showAllTokens + ? S.of(context).portfolioGlobal + : S.of(context).portfolio, + style: TextStyle( + fontSize: 13 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + height: 1.1), ), Text( - _showAllTokens ? '${dataManager.allTokens.length} ${S.of(context).tokens}' : '${dataManager.portfolio.length} ${S.of(context).tokens}', - style: TextStyle(fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), color: Colors.grey[600], height: 1.1), + _showAllTokens + ? '${dataManager.allTokens.length} ${S.of(context).tokens}' + : '${dataManager.portfolio.length} ${S.of(context).tokens}', + style: TextStyle( + fontSize: 11 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + color: Colors.grey[600], + height: 1.1), ), ], ), @@ -632,7 +700,7 @@ class MapsPageState extends State { ), ], ), - + // Switch pour whitelist avec indicateurs Row( children: [ @@ -645,8 +713,9 @@ class MapsPageState extends State { _showWhitelistedTokens = value; }); }, - activeColor: Theme.of(context).primaryColor, - trackColor: Colors.grey.shade300, + activeTrackColor: + Theme.of(context).primaryColor, + inactiveTrackColor: Colors.grey.shade300, ), ), const SizedBox(width: 4), @@ -655,12 +724,26 @@ class MapsPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - _showWhitelistedTokens ? S.of(context).showOnlyWhitelisted : S.of(context).showAll, - style: TextStyle(fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600, height: 1.1), + _showWhitelistedTokens + ? S.of(context).showOnlyWhitelisted + : S.of(context).showAll, + style: TextStyle( + fontSize: 13 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + height: 1.1), ), Text( _getWhitelistDescription(dataManager), - style: TextStyle(fontSize: 11 + Provider.of(context, listen: false).getTextSizeOffset(), color: Colors.grey[600], height: 1.1), + style: TextStyle( + fontSize: 11 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + color: Colors.grey[600], + height: 1.1), ), ], ), @@ -677,33 +760,49 @@ class MapsPageState extends State { children: [ ElevatedButton.icon( icon: Icon(Icons.filter_list, size: 16), - label: Text(S.of(context).filterOptions, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + label: Text(S.of(context).filterOptions, + style: TextStyle( + fontSize: 12 + + Provider.of(context, + listen: false) + .getTextSizeOffset())), onPressed: () { setState(() { _showFiltersPanel = !_showFiltersPanel; }); }, style: ElevatedButton.styleFrom( - backgroundColor: _showFiltersPanel ? Theme.of(context).primaryColor : Colors.grey.shade400, + backgroundColor: _showFiltersPanel + ? Theme.of(context).primaryColor + : Colors.grey.shade400, foregroundColor: Colors.white, minimumSize: Size(55, 26), - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), + padding: EdgeInsets.symmetric( + horizontal: 6, vertical: 3), ), ), const SizedBox(width: 6), ElevatedButton.icon( icon: Icon(Icons.dashboard, size: 16), - label: Text(S.of(context).statistics, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + label: Text(S.of(context).statistics, + style: TextStyle( + fontSize: 12 + + Provider.of(context, + listen: false) + .getTextSizeOffset())), onPressed: () { setState(() { _showMiniDashboard = !_showMiniDashboard; }); }, style: ElevatedButton.styleFrom( - backgroundColor: _showMiniDashboard ? Theme.of(context).primaryColor : Colors.grey.shade400, + backgroundColor: _showMiniDashboard + ? Theme.of(context).primaryColor + : Colors.grey.shade400, foregroundColor: Colors.white, minimumSize: Size(55, 26), - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), + padding: EdgeInsets.symmetric( + horizontal: 6, vertical: 3), ), ), ], @@ -726,13 +825,13 @@ class MapsPageState extends State { ), ), ), - + // Panneau de filtres avancés if (_showFiltersPanel) ...[ const SizedBox(height: 8), _buildFiltersPanel(context, dataManager), ], - + // Mini-dashboard (sans atténuation) if (_showMiniDashboard) ...[ const SizedBox(height: 8), @@ -741,14 +840,14 @@ class MapsPageState extends State { ], ), ), - ], ), ); } // Fonction améliorée pour déterminer la couleur et les stats du cluster - Map _getClusterStats(List markers, DataManager dataManager) { + Map _getClusterStats( + List markers, DataManager dataManager) { int fullyRented = 0; int notRented = 0; double totalValue = 0; @@ -768,7 +867,7 @@ class MapsPageState extends State { // Calculs pour les statistiques totalValue += tokenPrice; totalRent += _getTokenRentSafely(token['uuid'], dataManager); - + if (apy > 0) { totalApy += apy; apyCount++; @@ -810,37 +909,70 @@ class MapsPageState extends State { }; } - void _showClusterPopup(BuildContext context, Map clusterStats, int tokenCount, DataManager dataManager) { + void _showClusterPopup( + BuildContext context, + Map clusterStats, + int tokenCount, + DataManager dataManager) { final currencyUtils = Provider.of(context, listen: false); - + showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: Theme.of(context).cardColor, - title: Text('${S.of(context).tokens} ($tokenCount)', style: TextStyle(fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset())), + title: Text('${S.of(context).tokens} ($tokenCount)', + style: TextStyle( + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset())), content: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildInfoRow(S.of(context).totalTokens, '$tokenCount', Icons.location_on, Colors.blue), + _buildInfoRow(S.of(context).totalTokens, '$tokenCount', + Icons.location_on, Colors.blue), const Divider(height: 8), - _buildInfoRow(S.of(context).totalValue, - currencyUtils.formatCurrency(currencyUtils.convert(clusterStats['totalValue']), currencyUtils.currencySymbol), - Icons.attach_money, Colors.green), + _buildInfoRow( + S.of(context).totalValue, + currencyUtils.formatCurrency( + currencyUtils.convert(clusterStats['totalValue']), + currencyUtils.currencySymbol), + Icons.attach_money, + Colors.green), const Divider(height: 8), - _buildInfoRow(S.of(context).averageApy, - '${clusterStats['averageApy'].toStringAsFixed(2)}%', - Icons.trending_up, Colors.orange), + _buildInfoRow( + S.of(context).averageApy, + '${clusterStats['averageApy'].toStringAsFixed(2)}%', + Icons.trending_up, + Colors.orange), const Divider(height: 8), - _buildInfoRow(S.of(context).totalRent, - currencyUtils.formatCurrency(currencyUtils.convert(clusterStats['totalRent']), currencyUtils.currencySymbol), - Icons.account_balance, Colors.purple), + _buildInfoRow( + S.of(context).totalRent, + currencyUtils.formatCurrency( + currencyUtils.convert(clusterStats['totalRent']), + currencyUtils.currencySymbol), + Icons.account_balance, + Colors.purple), const Divider(height: 16), - Text(S.of(context).rentalStatusDistribution, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(S.of(context).rentalStatusDistribution, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset())), const SizedBox(height: 8), - _buildInfoRow(S.of(context).fullyRented, '${clusterStats['fullyRented']}', Icons.check_circle, Colors.green), - _buildInfoRow(S.of(context).partiallyRented, '${clusterStats['partiallyRented']}', Icons.pause_circle, Colors.orange), - _buildInfoRow(S.of(context).notRented, '${clusterStats['notRented']}', Icons.cancel, Colors.red), + _buildInfoRow( + S.of(context).fullyRented, + '${clusterStats['fullyRented']}', + Icons.check_circle, + Colors.green), + _buildInfoRow( + S.of(context).partiallyRented, + '${clusterStats['partiallyRented']}', + Icons.pause_circle, + Colors.orange), + _buildInfoRow(S.of(context).notRented, + '${clusterStats['notRented']}', Icons.cancel, Colors.red), ], ), actions: [ @@ -858,27 +990,37 @@ class MapsPageState extends State { // Récupérer le DataManager pour accéder au portefeuille et à la whitelist final dataManager = Provider.of(context, listen: false); final bool isInWallet = dataManager.portfolio.any( - (portfolioItem) => portfolioItem['uuid'].toLowerCase() == matchingToken['uuid'].toLowerCase(), + (portfolioItem) => + portfolioItem['uuid'].toLowerCase() == + matchingToken['uuid'].toLowerCase(), ); final bool isWhitelisted = dataManager.whitelistTokens.any( - (whitelisted) => whitelisted['token'].toLowerCase() == matchingToken['uuid'].toLowerCase(), + (whitelisted) => + whitelisted['token'].toLowerCase() == + matchingToken['uuid'].toLowerCase(), ); // Données financières enrichies final String tokenId = matchingToken['uuid'].toLowerCase(); - final double totalRentReceived = _getTokenRentSafely(matchingToken['uuid'], dataManager); + final double totalRentReceived = + _getTokenRentSafely(matchingToken['uuid'], dataManager); final int walletCount = dataManager.getWalletCountForToken(tokenId); final double yamTotalVolume = matchingToken['yamTotalVolume'] ?? 0.0; final double yamAverageValue = matchingToken['yamAverageValue'] ?? 0.0; - final List> transactions = dataManager.transactionsByToken[tokenId] ?? []; - final double initialValue = matchingToken['initialTotalValue'] ?? matchingToken['tokenPrice']; + final List> transactions = + dataManager.transactionsByToken[tokenId] ?? []; + final double initialValue = + matchingToken['initialTotalValue'] ?? matchingToken['tokenPrice']; final double currentValue = matchingToken['tokenPrice'] ?? 0.0; - final double roi = initialValue > 0 ? ((currentValue - initialValue) / initialValue * 100) : 0.0; + final double roi = initialValue > 0 + ? ((currentValue - initialValue) / initialValue * 100) + : 0.0; showDialog( context: context, builder: (BuildContext context) { - final currencyUtils = Provider.of(context, listen: false); + final currencyUtils = + Provider.of(context, listen: false); final rentedUnits = matchingToken['rentedUnits'] ?? 0; final totalUnits = matchingToken['totalUnits'] ?? 1; final lat = double.tryParse(matchingToken['lat']) ?? 0.0; @@ -898,7 +1040,8 @@ class MapsPageState extends State { imageUrl: matchingToken['imageLink'][0], width: 200, fit: BoxFit.cover, - placeholder: (context, url) => const CircularProgressIndicator(), + placeholder: (context, url) => + const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon( Icons.error, color: Colors.red, @@ -910,13 +1053,15 @@ class MapsPageState extends State { child: Text( matchingToken['shortName'], style: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), - + // Informations de base Card( margin: const EdgeInsets.symmetric(vertical: 4), @@ -924,17 +1069,27 @@ class MapsPageState extends State { padding: const EdgeInsets.all(12.0), child: Column( children: [ - _buildInfoRow(S.of(context).tokenPrice, - currencyUtils.formatCurrency(currencyUtils.convert(matchingToken['tokenPrice']), currencyUtils.currencySymbol), - Icons.monetization_on, Colors.green), + _buildInfoRow( + S.of(context).tokenPrice, + currencyUtils.formatCurrency( + currencyUtils + .convert(matchingToken['tokenPrice']), + currencyUtils.currencySymbol), + Icons.monetization_on, + Colors.green), const Divider(height: 8), - _buildInfoRow(S.of(context).annualPercentageYield, - '${matchingToken['annualPercentageYield'] != null ? matchingToken['annualPercentageYield'].toStringAsFixed(2) : 'N/A'}%', - Icons.trending_up, Colors.blue), + _buildInfoRow( + S.of(context).annualPercentageYield, + '${matchingToken['annualPercentageYield'] != null ? matchingToken['annualPercentageYield'].toStringAsFixed(2) : 'N/A'}%', + Icons.trending_up, + Colors.blue), const Divider(height: 8), - _buildInfoRow(S.of(context).rentedUnitsSimple, - '$rentedUnits / $totalUnits', - Icons.home, UIUtils.getRentalStatusColor(rentedUnits, totalUnits)), + _buildInfoRow( + S.of(context).rentedUnitsSimple, + '$rentedUnits / $totalUnits', + Icons.home, + UIUtils.getRentalStatusColor( + rentedUnits, totalUnits)), ], ), ), @@ -948,25 +1103,40 @@ class MapsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.of(context).finances, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(S.of(context).finances, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset())), const SizedBox(height: 8), - _buildInfoRow(S.of(context).totalRentReceived, - currencyUtils.formatCurrency(currencyUtils.convert(totalRentReceived), currencyUtils.currencySymbol), - Icons.account_balance, Colors.green), + _buildInfoRow( + S.of(context).totalRentReceived, + currencyUtils.formatCurrency( + currencyUtils.convert(totalRentReceived), + currencyUtils.currencySymbol), + Icons.account_balance, + Colors.green), const Divider(height: 8), - _buildInfoRow(S.of(context).averageROI, - '${roi.toStringAsFixed(2)}%', - Icons.show_chart, roi >= 0 ? Colors.green : Colors.red), + _buildInfoRow( + S.of(context).averageROI, + '${roi.toStringAsFixed(2)}%', + Icons.show_chart, + roi >= 0 ? Colors.green : Colors.red), const Divider(height: 8), - _buildInfoRow(S.of(context).walletsContainingToken, - '$walletCount', - Icons.account_balance_wallet, Colors.purple), + _buildInfoRow( + S.of(context).walletsContainingToken, + '$walletCount', + Icons.account_balance_wallet, + Colors.purple), if (transactions.isNotEmpty) ...[ const Divider(height: 8), - _buildInfoRow(S.of(context).transactionCount, - '${transactions.length}', - Icons.swap_horiz, Colors.orange), + _buildInfoRow( + S.of(context).transactionCount, + '${transactions.length}', + Icons.swap_horiz, + Colors.orange), ], ], ), @@ -982,16 +1152,29 @@ class MapsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.of(context).yam, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(S.of(context).yam, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14 + + Provider.of(context, + listen: false) + .getTextSizeOffset())), const SizedBox(height: 8), - _buildInfoRow(S.of(context).transactionVolume, - currencyUtils.formatCurrency(currencyUtils.convert(yamTotalVolume), currencyUtils.currencySymbol), - Icons.bar_chart, Colors.indigo), + _buildInfoRow( + S.of(context).transactionVolume, + currencyUtils.formatCurrency( + currencyUtils.convert(yamTotalVolume), + currencyUtils.currencySymbol), + Icons.bar_chart, + Colors.indigo), const Divider(height: 8), - _buildInfoRow(S.of(context).initialPrice, - currencyUtils.formatCurrency(currencyUtils.convert(yamAverageValue), currencyUtils.currencySymbol), - Icons.trending_flat, Colors.teal), + _buildInfoRow( + S.of(context).initialPrice, + currencyUtils.formatCurrency( + currencyUtils.convert(yamAverageValue), + currencyUtils.currencySymbol), + Icons.trending_flat, + Colors.teal), ], ), ), @@ -1009,13 +1192,18 @@ class MapsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - isInWallet ? Icons.account_balance_wallet : Icons.account_balance_wallet_outlined, + isInWallet + ? Icons.account_balance_wallet + : Icons.account_balance_wallet_outlined, color: isInWallet ? Colors.green : Colors.grey, ), const SizedBox(width: 8), Text( - isInWallet ? "Présent dans portefeuille" : "Non présent dans portefeuille", - style: const TextStyle(fontWeight: FontWeight.bold), + isInWallet + ? "Présent dans portefeuille" + : "Non présent dans portefeuille", + style: + const TextStyle(fontWeight: FontWeight.bold), ), ], ), @@ -1029,8 +1217,11 @@ class MapsPageState extends State { ), const SizedBox(width: 8), Text( - isWhitelisted ? "Token whitelisté" : "Token non whitelisté", - style: const TextStyle(fontWeight: FontWeight.bold), + isWhitelisted + ? "Token whitelisté" + : "Token non whitelisté", + style: + const TextStyle(fontWeight: FontWeight.bold), ), ], ), @@ -1060,7 +1251,8 @@ class MapsPageState extends State { icon: const Icon(Icons.streetview, size: 16), label: const Text('Street View'), onPressed: () { - final googleStreetViewUrl = 'https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=$lat,$lng'; + final googleStreetViewUrl = + 'https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=$lat,$lng'; UrlUtils.launchURL(googleStreetViewUrl); }, style: ElevatedButton.styleFrom( @@ -1078,15 +1270,26 @@ class MapsPageState extends State { ); } - Widget _buildInfoRow(String label, String value, IconData icon, Color iconColor) { + Widget _buildInfoRow( + String label, String value, IconData icon, Color iconColor) { return Row( children: [ Icon(icon, size: 18, color: iconColor), const SizedBox(width: 8), Expanded( - child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset())), + child: Text(label, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset())), ), - Text(value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(value, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset())), ], ); } @@ -1096,7 +1299,7 @@ class MapsPageState extends State { width: 300, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.95), + color: Theme.of(context).cardColor.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -1109,11 +1312,20 @@ class MapsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.of(context).filterOptions, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(S.of(context).filterOptions, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset())), const SizedBox(height: 12), - + // Filtre APY - Text('APY ($_minApy% - $_maxApy%)', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + Text('APY ($_minApy% - $_maxApy%)', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), RangeSlider( values: RangeValues(_minApy, _maxApy), min: 0, @@ -1126,10 +1338,14 @@ class MapsPageState extends State { }); }, ), - + // Filtres booléens CheckboxListTile( - title: Text(S.of(context).rents, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + title: Text(S.of(context).rents, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), value: _onlyWithRent, onChanged: (value) { setState(() { @@ -1138,9 +1354,13 @@ class MapsPageState extends State { }, dense: true, ), - + CheckboxListTile( - title: Text(S.of(context).fullyRented, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + title: Text(S.of(context).fullyRented, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), value: _onlyFullyRented, onChanged: (value) { setState(() { @@ -1149,17 +1369,30 @@ class MapsPageState extends State { }, dense: true, ), - + // Filtre par pays - Text(S.of(context).country, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w500)), + Text(S.of(context).country, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w500)), DropdownButton( value: _selectedCountry, - hint: Text(S.of(context).allCountries, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + hint: Text(S.of(context).allCountries, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), isExpanded: true, items: [ DropdownMenuItem( value: null, - child: Text(S.of(context).allCountries, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + child: Text(S.of(context).allCountries, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), ), ...dataManager.allTokens .map((token) => token['country']) @@ -1167,7 +1400,12 @@ class MapsPageState extends State { .toSet() .map((country) => DropdownMenuItem( value: country, - child: Text(country, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + child: Text(country, + style: TextStyle( + fontSize: 12 + + Provider.of(context, + listen: false) + .getTextSizeOffset())), )), ], onChanged: (value) { @@ -1176,10 +1414,14 @@ class MapsPageState extends State { }); }, ), - + // Filtre ROI const SizedBox(height: 8), - Text('ROI ($_minRoi% - $_maxRoi%)', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + Text('ROI ($_minRoi% - $_maxRoi%)', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), RangeSlider( values: RangeValues(_minRoi, _maxRoi), min: -100, @@ -1192,7 +1434,7 @@ class MapsPageState extends State { }); }, ), - + // Bouton de reset Center( child: ElevatedButton( @@ -1207,12 +1449,16 @@ class MapsPageState extends State { _maxRoi = 100.0; }); }, - child: Text('Reset', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, minimumSize: Size(80, 30), ), + child: Text('Reset', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), ), ), ], @@ -1220,10 +1466,11 @@ class MapsPageState extends State { ); } - Widget _buildMiniDashboard(BuildContext context, DataManager dataManager, List> displayedTokens) { + Widget _buildMiniDashboard(BuildContext context, DataManager dataManager, + List> displayedTokens) { final currencyUtils = Provider.of(context, listen: false); final appState = Provider.of(context, listen: false); - + // Calculer les propriétés uniques Set uniquePropertiesSet = {}; for (var token in displayedTokens) { @@ -1231,28 +1478,38 @@ class MapsPageState extends State { final lat = double.tryParse(token['lat'].toString()); final lng = double.tryParse(token['lng'].toString()); if (lat != null && lng != null) { - uniquePropertiesSet.add('${lat.toStringAsFixed(6)}_${lng.toStringAsFixed(6)}'); + uniquePropertiesSet + .add('${lat.toStringAsFixed(6)}_${lng.toStringAsFixed(6)}'); } } } - + // Calculer les statistiques final int totalTokens = displayedTokens.length; final int uniqueProperties = uniquePropertiesSet.length; - final double totalValue = displayedTokens.fold(0.0, (sum, token) => sum + (token['tokenPrice'] ?? 0.0)); - final double averageApy = displayedTokens.isNotEmpty - ? displayedTokens.fold(0.0, (sum, token) => sum + (token['annualPercentageYield'] ?? 0.0)) / totalTokens + final double totalValue = displayedTokens.fold( + 0.0, (sum, token) => sum + (token['tokenPrice'] ?? 0.0)); + final double averageApy = displayedTokens.isNotEmpty + ? displayedTokens.fold(0.0, + (sum, token) => sum + (token['annualPercentageYield'] ?? 0.0)) / + totalTokens : 0.0; - final double totalRent = displayedTokens.fold(0.0, (sum, token) => - sum + _getTokenRentSafely(token['uuid'], dataManager)); - - final int fullyRented = displayedTokens.where((token) => - (token['rentedUnits'] ?? 0) >= (token['totalUnits'] ?? 1)).length; - final int partiallyRented = displayedTokens.where((token) => - (token['rentedUnits'] ?? 0) > 0 && (token['rentedUnits'] ?? 0) < (token['totalUnits'] ?? 1)).length; - final int notRented = displayedTokens.where((token) => - (token['rentedUnits'] ?? 0) == 0).length; - + final double totalRent = displayedTokens.fold(0.0, + (sum, token) => sum + _getTokenRentSafely(token['uuid'], dataManager)); + + final int fullyRented = displayedTokens + .where((token) => + (token['rentedUnits'] ?? 0) >= (token['totalUnits'] ?? 1)) + .length; + final int partiallyRented = displayedTokens + .where((token) => + (token['rentedUnits'] ?? 0) > 0 && + (token['rentedUnits'] ?? 0) < (token['totalUnits'] ?? 1)) + .length; + final int notRented = displayedTokens + .where((token) => (token['rentedUnits'] ?? 0) == 0) + .length; + // Répartition par pays final Map countryDistribution = {}; for (var token in displayedTokens) { @@ -1264,7 +1521,7 @@ class MapsPageState extends State { width: 320, padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.95), + color: Theme.of(context).cardColor.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -1277,36 +1534,68 @@ class MapsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.of(context).statistics, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 + appState.getTextSizeOffset())), + Text(S.of(context).statistics, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16 + appState.getTextSizeOffset())), const SizedBox(height: 8), - + // Métriques principales - _buildStatRow(S.of(context).tokensInMap, '$totalTokens', Icons.location_on, totalTokens == 0 ? Colors.orange : Colors.blue), - _buildStatRow(S.of(context).totalProperties, '$uniqueProperties', Icons.map, Colors.indigo), - _buildStatRow(S.of(context).totalValue, currencyUtils.formatCurrency(currencyUtils.convert(totalValue), currencyUtils.currencySymbol), Icons.attach_money, Colors.green), - _buildStatRow(S.of(context).averageApy, '${averageApy.toStringAsFixed(2)}%', Icons.trending_up, Colors.orange), - _buildStatRow(S.of(context).totalRent, currencyUtils.formatCurrency(currencyUtils.convert(totalRent), currencyUtils.currencySymbol), Icons.account_balance, Colors.purple), - + _buildStatRow( + S.of(context).tokensInMap, + '$totalTokens', + Icons.location_on, + totalTokens == 0 ? Colors.orange : Colors.blue), + _buildStatRow(S.of(context).totalProperties, '$uniqueProperties', + Icons.map, Colors.indigo), + _buildStatRow( + S.of(context).totalValue, + currencyUtils.formatCurrency(currencyUtils.convert(totalValue), + currencyUtils.currencySymbol), + Icons.attach_money, + Colors.green), + _buildStatRow( + S.of(context).averageApy, + '${averageApy.toStringAsFixed(2)}%', + Icons.trending_up, + Colors.orange), + _buildStatRow( + S.of(context).totalRent, + currencyUtils.formatCurrency(currencyUtils.convert(totalRent), + currencyUtils.currencySymbol), + Icons.account_balance, + Colors.purple), + const Divider(height: 12), - + // Statut de location - Text(S.of(context).rentalStatus, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14 + appState.getTextSizeOffset())), + Text(S.of(context).rentalStatus, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14 + appState.getTextSizeOffset())), const SizedBox(height: 6), - _buildStatRow(S.of(context).fullyRented, '$fullyRented', Icons.check_circle, Colors.green), - _buildStatRow(S.of(context).partiallyRented, '$partiallyRented', Icons.pause_circle, Colors.orange), - _buildStatRow(S.of(context).notRented, '$notRented', Icons.cancel, Colors.red), - + _buildStatRow(S.of(context).fullyRented, '$fullyRented', + Icons.check_circle, Colors.green), + _buildStatRow(S.of(context).partiallyRented, '$partiallyRented', + Icons.pause_circle, Colors.orange), + _buildStatRow( + S.of(context).notRented, '$notRented', Icons.cancel, Colors.red), + const Divider(height: 12), - + // Répartition par pays (top 3) - Text(S.of(context).country, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14 + appState.getTextSizeOffset())), + Text(S.of(context).country, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14 + appState.getTextSizeOffset())), const SizedBox(height: 6), ...() { final sortedEntries = countryDistribution.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return sortedEntries .take(3) - .map((entry) => _buildStatRow(entry.key, '${entry.value}', Icons.flag, Colors.indigo)) + .map((entry) => _buildStatRow( + entry.key, '${entry.value}', Icons.flag, Colors.indigo)) .toList(); }(), ], @@ -1314,7 +1603,8 @@ class MapsPageState extends State { ); } - Widget _buildStatRow(String label, String value, IconData icon, Color iconColor) { + Widget _buildStatRow( + String label, String value, IconData icon, Color iconColor) { final appState = Provider.of(context, listen: false); return Padding( padding: const EdgeInsets.symmetric(vertical: 1), @@ -1323,9 +1613,13 @@ class MapsPageState extends State { Icon(icon, size: 15 + appState.getTextSizeOffset(), color: iconColor), const SizedBox(width: 6), Expanded( - child: Text(label, style: TextStyle(fontSize: 13 + appState.getTextSizeOffset())), + child: Text(label, + style: TextStyle(fontSize: 13 + appState.getTextSizeOffset())), ), - Text(value, style: TextStyle(fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold)), + Text(value, + style: TextStyle( + fontSize: 13 + appState.getTextSizeOffset(), + fontWeight: FontWeight.bold)), ], ), ); @@ -1333,13 +1627,17 @@ class MapsPageState extends State { String _getWhitelistDescription(DataManager dataManager) { if (_showWhitelistedTokens) { - final baseTokens = _showAllTokens ? dataManager.allTokens : dataManager.portfolio; - final whitelistedCount = baseTokens.where((token) => - dataManager.whitelistTokens.any((w) => w['token'].toLowerCase() == token['uuid'].toLowerCase()) - ).length; + final baseTokens = + _showAllTokens ? dataManager.allTokens : dataManager.portfolio; + final whitelistedCount = baseTokens + .where((token) => dataManager.whitelistTokens.any( + (w) => w['token'].toLowerCase() == token['uuid'].toLowerCase())) + .length; return '$whitelistedCount ${S.of(context).properties.toLowerCase()} whitelistées'; } else { - final baseCount = _showAllTokens ? dataManager.allTokens.length : dataManager.portfolio.length; + final baseCount = _showAllTokens + ? dataManager.allTokens.length + : dataManager.portfolio.length; return '$baseCount ${S.of(context).properties.toLowerCase()} disponibles'; } } @@ -1359,16 +1657,18 @@ class MapsPageState extends State { // Obtenir la couleur basée sur l'APY - progression rouge à vert de 0 à 12% Color _getApyBasedColor(double apy) { if (apy <= 0) return Colors.red.shade700; - + // Répartir rouge-vert de 0 à 12% avec granularité fine sur 9-12% if (apy < 3) { // Rouge intense à rouge-orange (0-3%) final ratio = (apy / 3.0).clamp(0.0, 1.0); - return Color.lerp(Colors.red.shade700, Colors.deepOrange.shade600, ratio)!; + return Color.lerp( + Colors.red.shade700, Colors.deepOrange.shade600, ratio)!; } else if (apy < 6) { // Rouge-orange à orange (3-6%) final ratio = ((apy - 3) / 3.0).clamp(0.0, 1.0); - return Color.lerp(Colors.deepOrange.shade600, Colors.orange.shade500, ratio)!; + return Color.lerp( + Colors.deepOrange.shade600, Colors.orange.shade500, ratio)!; } else if (apy < 9.0) { // Orange à jaune (6-9%) final ratio = ((apy - 6) / 3.0).clamp(0.0, 1.0); @@ -1380,11 +1680,13 @@ class MapsPageState extends State { } else if (apy < 10.0) { // Jaune-vert à vert clair (9.5-10.0%) final ratio = ((apy - 9.5) / 0.5).clamp(0.0, 1.0); - return Color.lerp(Colors.lime.shade500, Colors.lightGreen.shade500, ratio)!; + return Color.lerp( + Colors.lime.shade500, Colors.lightGreen.shade500, ratio)!; } else if (apy < 10.5) { // Vert clair à vert (10.0-10.5%) final ratio = ((apy - 10.0) / 0.5).clamp(0.0, 1.0); - return Color.lerp(Colors.lightGreen.shade500, Colors.green.shade500, ratio)!; + return Color.lerp( + Colors.lightGreen.shade500, Colors.green.shade500, ratio)!; } else if (apy < 11.0) { // Vert à vert moyen (10.5-11.0%) final ratio = ((apy - 10.5) / 0.5).clamp(0.0, 1.0); @@ -1444,7 +1746,7 @@ class MapsPageState extends State { ), ), const SizedBox(height: 12), - + // Option location RadioListTile( title: Text('État de location'), @@ -1461,19 +1763,32 @@ class MapsPageState extends State { secondary: Row( mainAxisSize: MainAxisSize.min, children: [ - Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.green)), + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.green)), const SizedBox(width: 4), - Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.orange)), + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.orange)), const SizedBox(width: 4), - Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.red)), + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.red)), ], ), ), - + // Option APY RadioListTile( title: Text('Rendement APY'), - subtitle: Text('Rouge→Vert (0-12%), granularité 0,5% sur 9-12%'), + subtitle: + Text('Rouge→Vert (0-12%), granularité 0,5% sur 9-12%'), value: ColorationMode.apy, groupValue: _colorationMode, onChanged: (value) { @@ -1490,16 +1805,16 @@ class MapsPageState extends State { borderRadius: BorderRadius.circular(6), gradient: LinearGradient( colors: [ - Colors.red.shade700, // 0-3% - Colors.deepOrange.shade600, // 3-6% - Colors.orange.shade500, // 6-9% - Colors.amber.shade600, // 9-9.5% - Colors.lime.shade500, // 9.5-10% - Colors.lightGreen.shade500, // 10-10.5% - Colors.green.shade500, // 10.5-11% - Colors.green.shade600, // 11-11.5% - Colors.green.shade700, // 11.5-12% - Colors.green.shade900, // 12%+ + Colors.red.shade700, // 0-3% + Colors.deepOrange.shade600, // 3-6% + Colors.orange.shade500, // 6-9% + Colors.amber.shade600, // 9-9.5% + Colors.lime.shade500, // 9.5-10% + Colors.lightGreen.shade500, // 10-10.5% + Colors.green.shade500, // 10.5-11% + Colors.green.shade600, // 11-11.5% + Colors.green.shade700, // 11.5-12% + Colors.green.shade900, // 12%+ ], ), ), @@ -1537,32 +1852,57 @@ class MapsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('Les 4 modes disponibles :', style: TextStyle(fontWeight: FontWeight.bold)), + Text('Les 4 modes disponibles :', + style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 12), - - _buildHelpRow('💼 ${S.of(context).portfolio}', 'Affiche uniquement vos ${S.of(context).properties.toLowerCase()}', 'OFF + OFF'), + _buildHelpRow( + '💼 ${S.of(context).portfolio}', + 'Affiche uniquement vos ${S.of(context).properties.toLowerCase()}', + 'OFF + OFF'), const Divider(), - _buildHelpRow('💼 Mes ${S.of(context).properties.toLowerCase()} whitelistées', 'Vos ${S.of(context).properties.toLowerCase()} dans la whitelist', 'OFF + ON'), + _buildHelpRow( + '💼 Mes ${S.of(context).properties.toLowerCase()} whitelistées', + 'Vos ${S.of(context).properties.toLowerCase()} dans la whitelist', + 'OFF + ON'), const Divider(), - _buildHelpRow('🌍 Toutes les ${S.of(context).properties.toLowerCase()}', 'Toutes les ${S.of(context).properties.toLowerCase()} du marché', 'ON + OFF'), + _buildHelpRow( + '🌍 Toutes les ${S.of(context).properties.toLowerCase()}', + 'Toutes les ${S.of(context).properties.toLowerCase()} du marché', + 'ON + OFF'), const Divider(), - _buildHelpRow('🌍 ${S.of(context).properties} whitelistées globales', 'Toutes les ${S.of(context).properties.toLowerCase()} whitelistées', 'ON + ON'), - + _buildHelpRow( + '🌍 ${S.of(context).properties} whitelistées globales', + 'Toutes les ${S.of(context).properties.toLowerCase()} whitelistées', + 'ON + ON'), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), + color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('💡 Conseils :', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)), + Text('💡 Conseils :', + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.blue)), const SizedBox(height: 4), - Text('• Utilisez les filtres pour affiner l\'analyse', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), - Text('• Cliquez sur Stats pour voir les métriques', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), - Text('• Les clusters montrent nombre + APY moyen', style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset())), + Text('• Utilisez les filtres pour affiner l\'analyse', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), + Text('• Cliquez sur Stats pour voir les métriques', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), + Text('• Les clusters montrent nombre + APY moyen', + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset())), ], ), ), @@ -1586,11 +1926,27 @@ class MapsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset())), + Text(title, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset())), const SizedBox(height: 2), - Text(description, style: TextStyle(fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), color: Colors.grey[600])), + Text(description, + style: TextStyle( + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + color: Colors.grey[600])), const SizedBox(height: 2), - Text('Switches: $switches', style: TextStyle(fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), color: Colors.grey[500], fontStyle: FontStyle.italic)), + Text('Switches: $switches', + style: TextStyle( + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + color: Colors.grey[500], + fontStyle: FontStyle.italic)), ], ), ); @@ -1605,47 +1961,51 @@ class MapsPageState extends State { }) { // Utiliser la couleur calculée selon le mode de coloration choisi final markerColor = color; - + // Validation et nettoyage de l'URL d'image String? getValidImageUrl() { try { if (matchingToken['imageLink'] == null) { - debugPrint("⚠️ imageLink est null pour ${matchingToken['shortName']}"); + debugPrint( + "⚠️ imageLink est null pour ${matchingToken['shortName']}"); return null; } - + var imageLink = matchingToken['imageLink']; String? imageUrl; - + if (imageLink is List && imageLink.isNotEmpty) { imageUrl = imageLink[0]?.toString(); } else if (imageLink is String && imageLink.isNotEmpty) { imageUrl = imageLink; } - + if (imageUrl == null || imageUrl.isEmpty) { debugPrint("⚠️ URL d'image vide pour ${matchingToken['shortName']}"); return null; } - + // Vérifier si l'URL est valide final uri = Uri.tryParse(imageUrl); - if (uri == null || (!uri.hasScheme || (!uri.scheme.startsWith('http')))) { - debugPrint("⚠️ URL d'image invalide pour ${matchingToken['shortName']}: $imageUrl"); + if (uri == null || + (!uri.hasScheme || (!uri.scheme.startsWith('http')))) { + debugPrint( + "⚠️ URL d'image invalide pour ${matchingToken['shortName']}: $imageUrl"); return null; } - - debugPrint("✅ URL d'image valide pour ${matchingToken['shortName']}: $imageUrl"); + + debugPrint( + "✅ URL d'image valide pour ${matchingToken['shortName']}: $imageUrl"); return imageUrl; - } catch (e) { - debugPrint("❌ Erreur lors de la validation de l'image pour ${matchingToken['shortName']}: $e"); + debugPrint( + "❌ Erreur lors de la validation de l'image pour ${matchingToken['shortName']}: $e"); return null; } } - + final String? validImageUrl = getValidImageUrl(); - + return Stack( alignment: Alignment.topCenter, children: [ @@ -1659,7 +2019,7 @@ class MapsPageState extends State { borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, offset: Offset(2, 2), ), @@ -1667,14 +2027,15 @@ class MapsPageState extends State { ), ), ), - + // Corps principal du pointeur Column( mainAxisSize: MainAxisSize.min, children: [ // Cercle avec la photo (décalé vers le bas) Transform.translate( - offset: Offset(0, 1.5), // Décale le cercle de 3 pixels vers le bas + offset: + Offset(0, 1.5), // Décale le cercle de 3 pixels vers le bas child: Container( width: 36, height: 36, @@ -1687,7 +2048,7 @@ class MapsPageState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 3, offset: Offset(0, 1), ), @@ -1708,17 +2069,18 @@ class MapsPageState extends State { fadeInDuration: Duration(milliseconds: 300), fadeOutDuration: Duration(milliseconds: 100), placeholder: (context, url) => Container( - color: markerColor.withOpacity(0.1), + color: markerColor.withValues(alpha: 0.1), child: Icon( Icons.home, - color: markerColor.withOpacity(0.7), + color: markerColor.withValues(alpha: 0.7), size: 16, ), ), errorWidget: (context, url, error) { - debugPrint("❌ Erreur chargement image pour ${matchingToken['shortName']}: $error"); + debugPrint( + "❌ Erreur chargement image pour ${matchingToken['shortName']}: $error"); return Container( - color: markerColor.withOpacity(0.1), + color: markerColor.withValues(alpha: 0.1), child: Icon( Icons.home, color: markerColor, @@ -1728,11 +2090,12 @@ class MapsPageState extends State { }, // Ajouter des headers pour contourner les problèmes CORS potentiels httpHeaders: { - 'User-Agent': 'Mozilla/5.0 (compatible; RealTokenApp/1.0)', + 'User-Agent': + 'Mozilla/5.0 (compatible; RealTokenApp/1.0)', }, ) : Container( - color: markerColor.withOpacity(0.1), + color: markerColor.withValues(alpha: 0.1), child: Icon( Icons.home, color: markerColor, @@ -1742,7 +2105,7 @@ class MapsPageState extends State { ), ), ), - + // Pointe du pointeur CustomPaint( size: Size(16, 12), @@ -1776,15 +2139,15 @@ class _PointerTipPainter extends CustomPainter { // Ombre de la pointe final shadowPaint = Paint() - ..color = Colors.black.withOpacity(0.3) + ..color = Colors.black.withValues(alpha: 0.3) ..style = PaintingStyle.fill; - + final shadowPath = ui.Path(); shadowPath.moveTo(size.width / 2 + 1, size.height + 1); shadowPath.lineTo(1, 1); shadowPath.lineTo(size.width + 1, 1); shadowPath.close(); - + canvas.drawPath(shadowPath, shadowPaint); canvas.drawPath(path, paint); } diff --git a/lib/pages/portfolio/FullScreenCarousel.dart b/lib/pages/portfolio/FullScreenCarousel.dart index 55ddee4..69edf61 100644 --- a/lib/pages/portfolio/FullScreenCarousel.dart +++ b/lib/pages/portfolio/FullScreenCarousel.dart @@ -32,7 +32,8 @@ class FullScreenCarousel extends StatelessWidget { enableInfiniteScroll: true, enlargeCenterPage: true, autoPlay: true, - autoPlayInterval: Duration(seconds: 2), // Défilement automatique après 2 secondes + autoPlayInterval: + Duration(seconds: 2), // Défilement automatique après 2 secondes ), items: imageLinks.map((imageUrl) { return InteractiveViewer( @@ -47,7 +48,8 @@ class FullScreenCarousel extends StatelessWidget { : CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.contain, - errorWidget: (context, url, error) => const Icon(Icons.error), + errorWidget: (context, url, error) => + const Icon(Icons.error), ), ); }).toList(), diff --git a/lib/pages/portfolio/portfolio_display_1.dart b/lib/pages/portfolio/portfolio_display_1.dart index ec52a49..617ed62 100644 --- a/lib/pages/portfolio/portfolio_display_1.dart +++ b/lib/pages/portfolio/portfolio_display_1.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:realtoken_asset_tracker/modals/token_details/showTokenDetails.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/location_utils.dart'; @@ -21,22 +20,23 @@ import 'dart:io'; class PortfolioDisplay1 extends StatelessWidget { final List> portfolio; final bool isLoading; - + const PortfolioDisplay1({ - super.key, + super.key, required this.portfolio, this.isLoading = false, }); /// Widget pour afficher une image avec gestion automatique de l'orientation - Widget _buildImageWithOrientation(String imageUrl, {BoxFit fit = BoxFit.cover}) { + Widget _buildImageWithOrientation(String imageUrl, + {BoxFit fit = BoxFit.cover}) { if (kIsWeb) { return ShowNetworkImage( imageSrc: imageUrl, mobileBoxFit: fit, ); } - + // Pour Android, utiliser Image.network avec filterQuality pour une meilleure gestion if (Platform.isAndroid) { return Image.network( @@ -60,14 +60,15 @@ class PortfolioDisplay1 extends StatelessWidget { return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! : null, ), ); }, ); } - + // Pour iOS et autres plateformes, utiliser CachedNetworkImage return CachedNetworkImage( imageUrl: imageUrl, @@ -86,8 +87,10 @@ class PortfolioDisplay1 extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 2), child: LayoutBuilder( builder: (context, constraints) { - double maxWidth = constraints.maxWidth; // Utiliser la largeur complète - final bool showTextInside = rentValue >= 15; // Afficher le texte à l'intérieur seulement si >= 15% + double maxWidth = + constraints.maxWidth; // Utiliser la largeur complète + final bool showTextInside = rentValue >= + 15; // Afficher le texte à l'intérieur seulement si >= 15% return Stack( children: [ @@ -96,18 +99,19 @@ class PortfolioDisplay1 extends StatelessWidget { height: 14, width: maxWidth, decoration: BoxDecoration( - color: Theme.of(context).dividerColor.withOpacity(0.2), + color: Theme.of(context).dividerColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), ), // Partie remplie de la jauge Container( height: 14, - width: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8), // Largeur minimum de 8px pour garantir les bords arrondis + width: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, + 8), // Largeur minimum de 8px pour garantir les bords arrondis decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Theme.of(context).primaryColor.withOpacity(0.7), + Theme.of(context).primaryColor.withValues(alpha: 0.7), Theme.of(context).primaryColor, ], begin: Alignment.centerLeft, @@ -123,70 +127,88 @@ class PortfolioDisplay1 extends StatelessWidget { // Texte du pourcentage à l'intérieur de la barre seulement si assez d'espace child: showTextInside ? Center( - child: isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + child: isLoading + ? ShimmerUtils.originalColorShimmer( + child: Text( + "${rentValue.toStringAsFixed(1)}%", + style: TextStyle( + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 1, + color: + Colors.black.withValues(alpha: 0.5), + ), + ], + ), + ), + color: Colors.white, + ) + : Text( "${rentValue.toStringAsFixed(1)}%", style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: Colors.white, shadows: [ Shadow( offset: const Offset(0, 1), blurRadius: 1, - color: Colors.black.withOpacity(0.5), + color: + Colors.black.withValues(alpha: 0.5), ), ], ), ), - color: Colors.white, - ) - : Text( - "${rentValue.toStringAsFixed(1)}%", - style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Colors.white, - shadows: [ - Shadow( - offset: const Offset(0, 1), - blurRadius: 1, - color: Colors.black.withOpacity(0.5), - ), - ], - ), - ), ) : null, ), // Texte du pourcentage visible en dehors de la barre seulement si < 15% if (!showTextInside) Positioned( - left: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8) + 4, + left: + Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8) + 4, top: 0, bottom: 0, child: Center( child: isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils.originalColorShimmer( + child: Text( + "${rentValue.toStringAsFixed(1)}%", + style: TextStyle( + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ), + ), + color: Theme.of(context).textTheme.bodyLarge?.color, + ) + : Text( "${rentValue.toStringAsFixed(1)}%", style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: + Theme.of(context).textTheme.bodyLarge?.color, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - "${rentValue.toStringAsFixed(1)}%", - style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), ), ), ], @@ -225,15 +247,18 @@ class PortfolioDisplay1 extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const ManageEvmAddressesPage(), + builder: (context) => + const ManageEvmAddressesPage(), ), ); }, style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Theme.of(context).primaryColor, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), ), child: Text( S.of(context).manageAddresses, @@ -248,7 +273,8 @@ class PortfolioDisplay1 extends StatelessWidget { ), ) : GridView.builder( - padding: const EdgeInsets.only(top: 8, bottom: 80, right: 16, left: 16), + padding: const EdgeInsets.only( + top: 8, bottom: 80, right: 16, left: 16), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: MediaQuery.of(context).size.width > 700 ? 2 : 1, crossAxisSpacing: 16, @@ -263,25 +289,35 @@ class PortfolioDisplay1 extends StatelessWidget { final city = LocationUtils.extractCity(token['fullName'] ?? ''); // Vérifier si la date de 'rent_start' est dans le futur - final rentStartDate = DateTime.parse(token['rentStartDate'] ?? DateTime.now().toString()); - final bool isFutureRentStart = rentStartDate.isAfter(DateTime.now()); + final rentStartDate = DateTime.parse( + token['rentStartDate'] ?? DateTime.now().toString()); + final bool isFutureRentStart = + rentStartDate.isAfter(DateTime.now()); // Récupérer les loyers cumulés de tous les wallets pour ce token String tokenId = token['uuid']?.toLowerCase() ?? ''; - double totalRentReceived = tokenId.isNotEmpty ? dataManager.cumulativeRentsByToken[tokenId] ?? token['totalRentReceived'] ?? 0 : token['totalRentReceived'] ?? 0; + double totalRentReceived = tokenId.isNotEmpty + ? dataManager.cumulativeRentsByToken[tokenId] ?? + token['totalRentReceived'] ?? + 0 + : token['totalRentReceived'] ?? 0; - final rentPercentage = - (totalRentReceived != null && token['initialTotalValue'] != null && token['initialTotalValue'] != 0) ? (totalRentReceived / token['initialTotalValue']) * 100 : 0.5; + final rentPercentage = (token['initialTotalValue'] != null && + token['initialTotalValue'] != 0) + ? (totalRentReceived / token['initialTotalValue']) * 100 + : 0.5; // Récupérer le nombre de wallets possédant ce token - int walletCount = tokenId.isNotEmpty ? dataManager.getWalletCountForToken(tokenId) : 0; + int walletCount = tokenId.isNotEmpty + ? dataManager.getWalletCountForToken(tokenId) + : 0; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.06), + color: Colors.black.withValues(alpha: 0.06), blurRadius: 12, spreadRadius: 0, offset: const Offset(0, 4), @@ -306,7 +342,12 @@ class PortfolioDisplay1 extends StatelessWidget { child: Stack( children: [ ColorFiltered( - colorFilter: isFutureRentStart ? const ColorFilter.mode(Colors.black45, BlendMode.darken) : const ColorFilter.mode(Colors.transparent, BlendMode.multiply), + colorFilter: isFutureRentStart + ? const ColorFilter.mode( + Colors.black45, BlendMode.darken) + : const ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), child: SizedBox( width: kIsWeb ? 150 : 120, height: double.infinity, @@ -336,7 +377,10 @@ class PortfolioDisplay1 extends StatelessWidget { child: WidgetFactory.buildImageOverlay( context: context, text: S.of(context).rentStartFuture, - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -351,15 +395,23 @@ class PortfolioDisplay1 extends StatelessWidget { WidgetFactory.buildImageIndicator( context: context, text: S.of(context).wallet, - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), const SizedBox(width: 6), if (isRMM) WidgetFactory.buildImageIndicator( context: context, text: 'RMM', - backgroundColor: const Color.fromARGB(150, 165, 100, 21), - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + backgroundColor: + const Color.fromARGB( + 150, 165, 100, 21), + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), ), ], ), @@ -370,16 +422,19 @@ class PortfolioDisplay1 extends StatelessWidget { right: 0, child: ClipRRect( child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + filter: ImageFilter.blur( + sigmaX: 3, sigmaY: 3), child: Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 10), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, - Colors.black.withOpacity(0.6), + Colors.black + .withValues(alpha: 0.6), ], ), ), @@ -390,16 +445,20 @@ class PortfolioDisplay1 extends StatelessWidget { height: 10, decoration: BoxDecoration( shape: BoxShape.circle, - color: UIUtils.getRentalStatusColor( + color: UIUtils + .getRentalStatusColor( token['rentedUnits'] ?? 0, token['totalUnits'] ?? 1, ), boxShadow: [ BoxShadow( - color: UIUtils.getRentalStatusColor( - token['rentedUnits'] ?? 0, - token['totalUnits'] ?? 1, - ).withOpacity(0.5), + color: UIUtils + .getRentalStatusColor( + token['rentedUnits'] ?? + 0, + token['totalUnits'] ?? + 1, + ).withValues(alpha: 0.5), blurRadius: 6, spreadRadius: 0, ), @@ -412,13 +471,18 @@ class PortfolioDisplay1 extends StatelessWidget { city, style: TextStyle( color: Colors.white, - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: 14 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w600, shadows: [ Shadow( - offset: const Offset(0, 1), + offset: + const Offset(0, 1), blurRadius: 2, - color: Colors.black.withOpacity(0.5), + color: Colors.black + .withValues( + alpha: 0.5), ), ], ), @@ -436,42 +500,64 @@ class PortfolioDisplay1 extends StatelessWidget { ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ if (token['country'] != null) Padding( - padding: const EdgeInsets.only(right: 6), + padding: + const EdgeInsets.only( + right: 6), child: ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: + BorderRadius.circular( + 4), child: Image.asset( 'assets/country/${token['country'].toLowerCase()}.png', - width: 22 + appState.getTextSizeOffset(), - height: 22 + appState.getTextSizeOffset(), - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.flag, size: 22); + width: 22 + + appState + .getTextSizeOffset(), + height: 22 + + appState + .getTextSizeOffset(), + errorBuilder: (context, + error, stackTrace) { + return const Icon( + Icons.flag, + size: 22); }, ), ), ), Expanded( child: Text( - token['shortName'] ?? S.of(context).nameUnavailable, + token['shortName'] ?? + S + .of(context) + .nameUnavailable, style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), + fontSize: 16 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, letterSpacing: -0.2, ), maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: + TextOverflow.ellipsis, ), ), ], @@ -482,168 +568,312 @@ class PortfolioDisplay1 extends StatelessWidget { const SizedBox(height: 6), SizedBox( width: double.infinity, - child: rentPercentage != null ? _buildGaugeForRent(rentPercentage, context) : const SizedBox.shrink(), + child: rentPercentage != null + ? _buildGaugeForRent( + rentPercentage, context) + : const SizedBox.shrink(), ), const SizedBox(height: 2), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( - child: isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + child: isLoading + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', + style: TextStyle( + fontSize: 12 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w500, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + ), + ), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + ) + : Text( '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), + fontSize: 12 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), + overflow: + TextOverflow.ellipsis, ), - color: Theme.of(context).textTheme.bodySmall?.color, - ) - : Text( - '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - overflow: TextOverflow.ellipsis, - ), ), Row( children: [ Icon( Icons.trending_up, - size: 14 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor, + size: 14 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .primaryColor, ), const SizedBox(width: 2), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + color: Theme.of(context) + .primaryColor, + ), + ), + color: Theme.of(context) + .primaryColor, + ) + : Text( '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryColor, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + color: Theme.of(context) + .primaryColor, ), ), - color: Theme.of(context).primaryColor, - ) - : Text( - '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryColor, - ), - ), ], ), ], ), const SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 6), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).totalValue, style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 11 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, fontWeight: FontWeight.w500, ), ), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert(token['totalValue']), - currencyUtils.currencySymbol, + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils + .getFormattedAmount( + currencyUtils.convert( + token[ + 'totalValue']), + currencyUtils + .currencySymbol, + appState.showAmounts, + ), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + ), + ), + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils + .getFormattedAmount( + currencyUtils.convert( + token[ + 'totalValue']), + currencyUtils + .currencySymbol, appState.showAmounts, ), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert(token['totalValue']), - currencyUtils.currencySymbol, - appState.showAmounts, - ), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.end, children: [ Text( 'YAM', style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 11 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, fontWeight: FontWeight.w500, ), ), Row( children: [ isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert((token[ + 'yamAverageValue'] * + token[ + 'amount'])), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, + ), + ), + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors + .red.shade600, + ) + : Text( currencyUtils.getFormattedAmount( - currencyUtils.convert((token['yamAverageValue'] * token['amount'])), currencyUtils.currencySymbol, appState.showAmounts), + currencyUtils + .convert((token[ + 'yamAverageValue'] * + token[ + 'amount'])), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, ), ), - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ) - : Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert((token['yamAverageValue'] * token['amount'])), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ), - ), const SizedBox(width: 4), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', + style: TextStyle( + fontSize: 11 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w500, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, + ), + ), + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors + .red.shade600, + ) + : Text( '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, + fontSize: 11 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w500, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, ), ), - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ) - : Text( - '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', - style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ), - ), ], ), ], @@ -654,112 +884,217 @@ class PortfolioDisplay1 extends StatelessWidget { const Spacer(), Container( decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.08), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 2), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).revenue, style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 13 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor, + fontSize: 13 + + appState.getTextSizeOffset(), + color: Theme.of(context) + .primaryColor, ), ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).week, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['dailyIncome'] * 7), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert( + token['dailyIncome'] * + 7), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: + Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'dailyIncome'] * + 7), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['dailyIncome'] * 7), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).month, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['monthlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'monthlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: + Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'monthlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['monthlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).year, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['yearlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'yearlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: + Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'yearlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['yearlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), ], diff --git a/lib/pages/portfolio/portfolio_display_2.dart b/lib/pages/portfolio/portfolio_display_2.dart index ccc169d..1b14a1c 100644 --- a/lib/pages/portfolio/portfolio_display_2.dart +++ b/lib/pages/portfolio/portfolio_display_2.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:realtoken_asset_tracker/modals/token_details/showTokenDetails.dart'; import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; @@ -22,7 +21,7 @@ class PortfolioDisplay2 extends StatefulWidget { final bool isLoading; const PortfolioDisplay2({ - super.key, + super.key, required this.portfolio, this.isLoading = false, }); @@ -33,14 +32,15 @@ class PortfolioDisplay2 extends StatefulWidget { class PortfolioDisplay2State extends State { /// Widget pour afficher une image avec gestion automatique de l'orientation - Widget _buildImageWithOrientation(String imageUrl, {BoxFit fit = BoxFit.cover}) { + Widget _buildImageWithOrientation(String imageUrl, + {BoxFit fit = BoxFit.cover}) { if (kIsWeb) { return ShowNetworkImage( imageSrc: imageUrl, mobileBoxFit: fit, ); } - + // Pour Android, utiliser Image.network avec filterQuality pour une meilleure gestion if (Platform.isAndroid) { return Image.network( @@ -64,14 +64,15 @@ class PortfolioDisplay2State extends State { return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! : null, ), ); }, ); } - + // Pour iOS et autres plateformes, utiliser CachedNetworkImage return CachedNetworkImage( imageUrl: imageUrl, @@ -90,7 +91,8 @@ class PortfolioDisplay2State extends State { child: LayoutBuilder( builder: (context, constraints) { double maxWidth = constraints.maxWidth; - final bool showTextInside = rentValue >= 15; // Afficher le texte à l'intérieur seulement si >= 15% + final bool showTextInside = rentValue >= + 15; // Afficher le texte à l'intérieur seulement si >= 15% return Stack( children: [ @@ -99,18 +101,19 @@ class PortfolioDisplay2State extends State { height: 16, width: maxWidth, decoration: BoxDecoration( - color: Theme.of(context).dividerColor.withOpacity(0.2), + color: Theme.of(context).dividerColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), ), // Partie remplie de la jauge Container( height: 16, - width: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8), // Largeur minimum pour garantir les bords arrondis + width: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, + 8), // Largeur minimum pour garantir les bords arrondis decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Theme.of(context).primaryColor.withOpacity(0.7), + Theme.of(context).primaryColor.withValues(alpha: 0.7), Theme.of(context).primaryColor, ], begin: Alignment.centerLeft, @@ -121,70 +124,88 @@ class PortfolioDisplay2State extends State { // Texte du pourcentage à l'intérieur de la barre seulement si assez d'espace child: showTextInside ? Center( - child: widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + child: widget.isLoading + ? ShimmerUtils.originalColorShimmer( + child: Text( + "${rentValue.toStringAsFixed(1)}%", + style: TextStyle( + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 1, + color: + Colors.black.withValues(alpha: 0.5), + ), + ], + ), + ), + color: Colors.white, + ) + : Text( "${rentValue.toStringAsFixed(1)}%", style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: Colors.white, shadows: [ Shadow( offset: const Offset(0, 1), blurRadius: 1, - color: Colors.black.withOpacity(0.5), + color: + Colors.black.withValues(alpha: 0.5), ), ], ), ), - color: Colors.white, - ) - : Text( - "${rentValue.toStringAsFixed(1)}%", - style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Colors.white, - shadows: [ - Shadow( - offset: const Offset(0, 1), - blurRadius: 1, - color: Colors.black.withOpacity(0.5), - ), - ], - ), - ), ) : null, ), // Texte du pourcentage visible en dehors de la barre seulement si < 15% if (!showTextInside) Positioned( - left: Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8) + 4, + left: + Math.max(rentValue.clamp(0, 100) / 100 * maxWidth, 8) + 4, top: 0, bottom: 0, child: Center( child: widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils.originalColorShimmer( + child: Text( + "${rentValue.toStringAsFixed(1)}%", + style: TextStyle( + fontSize: 10 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ), + ), + color: Theme.of(context).textTheme.bodyLarge?.color, + ) + : Text( "${rentValue.toStringAsFixed(1)}%", style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: + Theme.of(context).textTheme.bodyLarge?.color, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - "${rentValue.toStringAsFixed(1)}%", - style: TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), ), ), ], @@ -227,15 +248,18 @@ class PortfolioDisplay2State extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const ManageEvmAddressesPage(), + builder: (context) => + const ManageEvmAddressesPage(), ), ); }, style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Theme.of(context).primaryColor, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), ), child: Text( S.of(context).manageAddresses, @@ -252,7 +276,8 @@ class PortfolioDisplay2State extends State { ) : Expanded( child: AlignedGridView.count( - padding: const EdgeInsets.only(top: 8, bottom: 80, right: 16, left: 16), + padding: const EdgeInsets.only( + top: 8, bottom: 80, right: 16, left: 16), crossAxisCount: widthScreen > 700 ? 2 : 1, mainAxisSpacing: 16, crossAxisSpacing: 16, @@ -261,21 +286,29 @@ class PortfolioDisplay2State extends State { final token = filteredPortfolio[index]; final isWallet = token['inWallet'] ?? false; final isRMM = token['inRMM'] ?? false; - final city = LocationUtils.extractCity(token['fullName'] ?? ''); + final city = + LocationUtils.extractCity(token['fullName'] ?? ''); - final rentStartDate = DateTime.tryParse(token['rentStartDate'] ?? ''); - final bool isFutureRentStart = rentStartDate != null && rentStartDate.isAfter(DateTime.now()); + final rentStartDate = + DateTime.tryParse(token['rentStartDate'] ?? ''); + final bool isFutureRentStart = rentStartDate != null && + rentStartDate.isAfter(DateTime.now()); - final rentPercentage = (token['totalRentReceived'] != null && token['initialTotalValue'] != null && token['initialTotalValue'] != 0) - ? (token['totalRentReceived'] / token['initialTotalValue']) * 100 - : 0.5; + final rentPercentage = + (token['totalRentReceived'] != null && + token['initialTotalValue'] != null && + token['initialTotalValue'] != 0) + ? (token['totalRentReceived'] / + token['initialTotalValue']) * + 100 + : 0.5; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.06), + color: Colors.black.withValues(alpha: 0.06), blurRadius: 12, spreadRadius: 0, offset: const Offset(0, 4), @@ -306,7 +339,13 @@ class PortfolioDisplay2State extends State { topRight: Radius.circular(20), ), child: ColorFiltered( - colorFilter: isFutureRentStart ? const ColorFilter.mode(Colors.black45, BlendMode.darken) : const ColorFilter.mode(Colors.transparent, BlendMode.multiply), + colorFilter: isFutureRentStart + ? const ColorFilter.mode( + Colors.black45, + BlendMode.darken) + : const ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), child: _buildImageWithOrientation( token['imageLink'][0], fit: BoxFit.cover, @@ -318,7 +357,8 @@ class PortfolioDisplay2State extends State { if (kIsWeb) Positioned.fill( child: GestureDetector( - behavior: HitTestBehavior.translucent, + behavior: + HitTestBehavior.translucent, onTap: () { showTokenDetails(context, token); }, @@ -329,11 +369,19 @@ class PortfolioDisplay2State extends State { Positioned.fill( child: Center( child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: WidgetFactory.buildImageOverlay( + borderRadius: + BorderRadius.circular(10), + child: WidgetFactory + .buildImageOverlay( context: context, - text: S.of(context).rentStartFuture, - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + text: S + .of(context) + .rentStartFuture, + fontSize: 12 + + Provider.of( + context, + listen: false) + .getTextSizeOffset(), ), ), ), @@ -347,18 +395,30 @@ class PortfolioDisplay2State extends State { child: Row( children: [ if (isWallet) - WidgetFactory.buildImageIndicator( + WidgetFactory + .buildImageIndicator( context: context, text: S.of(context).wallet, - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 10 + + Provider.of( + context, + listen: false) + .getTextSizeOffset(), ), const SizedBox(width: 6), if (isRMM) - WidgetFactory.buildImageIndicator( + WidgetFactory + .buildImageIndicator( context: context, text: 'RMM', - backgroundColor: const Color.fromARGB(150, 165, 100, 21), - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset(), + backgroundColor: + const Color.fromARGB( + 150, 165, 100, 21), + fontSize: 10 + + Provider.of( + context, + listen: false) + .getTextSizeOffset(), ), ], ), @@ -370,16 +430,21 @@ class PortfolioDisplay2State extends State { right: 0, child: ClipRRect( child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + filter: ImageFilter.blur( + sigmaX: 3, sigmaY: 3), child: Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + padding: + const EdgeInsets.symmetric( + vertical: 8, + horizontal: 10), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, - Colors.black.withOpacity(0.6), + Colors.black + .withValues(alpha: 0.6), ], ), ), @@ -387,13 +452,18 @@ class PortfolioDisplay2State extends State { city, style: TextStyle( color: Colors.white, - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: 14 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w600, shadows: [ Shadow( - offset: const Offset(0, 1), + offset: + const Offset(0, 1), blurRadius: 2, - color: Colors.black.withOpacity(0.5), + color: Colors.black + .withValues( + alpha: 0.5), ), ], ), @@ -408,32 +478,50 @@ class PortfolioDisplay2State extends State { Padding( padding: const EdgeInsets.all(12.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Row( children: [ if (token['country'] != null) Padding( - padding: const EdgeInsets.only(right: 6), + padding: const EdgeInsets.only( + right: 6), child: ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: + BorderRadius.circular(4), child: Image.asset( 'assets/country/${token['country'].toLowerCase()}.png', - width: 22 + appState.getTextSizeOffset(), - height: 22 + appState.getTextSizeOffset(), - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.flag, size: 22); + width: 22 + + appState + .getTextSizeOffset(), + height: 22 + + appState + .getTextSizeOffset(), + errorBuilder: (context, + error, stackTrace) { + return const Icon( + Icons.flag, + size: 22); }, ), ), ), Expanded( child: Text( - token['shortName'] ?? S.of(context).nameUnavailable, + token['shortName'] ?? + S + .of(context) + .nameUnavailable, style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), + fontSize: 16 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, letterSpacing: -0.2, ), maxLines: 1, @@ -444,64 +532,99 @@ class PortfolioDisplay2State extends State { ), const SizedBox(height: 8), // Barre de progression - _buildGaugeForRent(rentPercentage, context), + _buildGaugeForRent( + rentPercentage, context), const SizedBox(height: 2), // Montant et APY Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', + style: TextStyle( + fontSize: 12 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w500, + color: + Theme.of(context) + .textTheme + .bodySmall + ?.color, + ), + ), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + ) + : Text( '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w500, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), + overflow: + TextOverflow.ellipsis, ), - color: Theme.of(context).textTheme.bodySmall?.color, - ) - : Text( - '${token['amount']?.toStringAsFixed(2) ?? '0.00'} / ${token['totalTokens'] ?? 'N/A'}', - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - overflow: TextOverflow.ellipsis, - ), ), Row( children: [ Icon( Icons.trending_up, - size: 14 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor, + size: 14 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .primaryColor, ), const SizedBox(width: 2), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + color: Theme.of( + context) + .primaryColor, + ), + ), + color: Theme.of(context) + .primaryColor, + ) + : Text( '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryColor, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, + color: + Theme.of(context) + .primaryColor, ), ), - color: Theme.of(context).primaryColor, - ) - : Text( - '${token['annualPercentageYield']?.toStringAsFixed(2) ?? 'N/A'}%', - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryColor, - ), - ), ], ), ], @@ -510,105 +633,232 @@ class PortfolioDisplay2State extends State { // Valeurs totales Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 6), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).totalValue, style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, - fontWeight: FontWeight.w500, + fontSize: 11 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + fontWeight: + FontWeight.w500, ), ), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert(token['totalValue']), - currencyUtils.currencySymbol, - appState.showAmounts, + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils + .getFormattedAmount( + currencyUtils + .convert(token[ + 'totalValue']), + currencyUtils + .currencySymbol, + appState + .showAmounts, + ), + style: TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: + Theme.of(context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils + .getFormattedAmount( + currencyUtils + .convert(token[ + 'totalValue']), + currencyUtils + .currencySymbol, + appState + .showAmounts, ), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight.w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert(token['totalValue']), - currencyUtils.currencySymbol, - appState.showAmounts, - ), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.end, children: [ Text( 'YAM', style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, - fontWeight: FontWeight.w500, + fontSize: 11 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + fontWeight: + FontWeight.w500, ), ), Row( children: [ widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils.convert((token[ + 'yamAverageValue'] * + token[ + 'amount'])), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: + TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors + .green + .shade600 + : Colors + .red + .shade600, + ), + ), + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, + ) + : Text( currencyUtils.getFormattedAmount( - currencyUtils.convert((token['yamAverageValue'] * token['amount'])), currencyUtils.currencySymbol, appState.showAmounts), + currencyUtils + .convert((token[ + 'yamAverageValue'] * + token[ + 'amount'])), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors + .green + .shade600 + : Colors.red + .shade600, ), ), - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ) - : Text( - currencyUtils.getFormattedAmount( - currencyUtils.convert((token['yamAverageValue'] * token['amount'])), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ), - ), const SizedBox(width: 4), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( + ? ShimmerUtils + .originalColorShimmer( + child: Text( + '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', + style: + TextStyle( + fontSize: 11 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w500, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors + .green + .shade600 + : Colors + .red + .shade600, + ), + ), + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors.green + .shade600 + : Colors.red + .shade600, + ) + : Text( '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, + fontSize: 11 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w500, + color: (token['yamAverageValue'] * + token[ + 'amount']) >= + token[ + 'totalValue'] + ? Colors + .green + .shade600 + : Colors.red + .shade600, ), ), - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ) - : Text( - '(${((token['yamAverageValue'] / token['tokenPrice'] - 1) * 100).toStringAsFixed(0)}%)', - style: TextStyle( - fontSize: 11 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: (token['yamAverageValue'] * token['amount']) >= token['totalValue'] ? Colors.green.shade600 : Colors.red.shade600, - ), - ), ], ), ], @@ -621,113 +871,235 @@ class PortfolioDisplay2State extends State { // Section Revenus Container( decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(12), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.08), + borderRadius: + BorderRadius.circular(12), ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 8), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( S.of(context).revenue, style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 13 + appState.getTextSizeOffset(), - color: Theme.of(context).primaryColor, + fontSize: 13 + + appState + .getTextSizeOffset(), + color: Theme.of(context) + .primaryColor, ), ), const SizedBox(height: 4), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment + .start, children: [ Text( S.of(context).week, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: + Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['dailyIncome'] * 7), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert( + token['dailyIncome'] * + 7), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: + TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: Theme.of( + context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert( + token['dailyIncome'] * + 7), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['dailyIncome'] * 7), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment + .start, children: [ Text( S.of(context).month, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: + Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['monthlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'monthlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: + TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: Theme.of( + context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert( + token[ + 'monthlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['monthlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment + .start, children: [ Text( S.of(context).year, style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 12 + + appState + .getTextSizeOffset(), + color: + Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), widget.isLoading - ? ShimmerUtils.originalColorShimmer( - child: Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['yearlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), + ? ShimmerUtils + .originalColorShimmer( + child: Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert(token[ + 'yearlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), + style: + TextStyle( + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, + ), + ), + color: Theme.of( + context) + .textTheme + .bodyLarge + ?.color, + ) + : Text( + currencyUtils.getFormattedAmount( + currencyUtils + .convert( + token[ + 'yearlyIncome']), + currencyUtils + .currencySymbol, + appState + .showAmounts), style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, + fontSize: 13 + + appState + .getTextSizeOffset(), + fontWeight: + FontWeight + .w600, ), ), - color: Theme.of(context).textTheme.bodyLarge?.color, - ) - : Text( - currencyUtils.getFormattedAmount(currencyUtils.convert(token['yearlyIncome']), currencyUtils.currencySymbol, appState.showAmounts), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - ), - ), ], ), ], diff --git a/lib/pages/portfolio/portfolio_page.dart b/lib/pages/portfolio/portfolio_page.dart index 23e19cf..1755d25 100644 --- a/lib/pages/portfolio/portfolio_page.dart +++ b/lib/pages/portfolio/portfolio_page.dart @@ -1,8 +1,6 @@ -import 'package:googleapis/meet/v2.dart'; import 'package:realtoken_asset_tracker/utils/data_fetch_utils.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:realtoken_asset_tracker/utils/text_utils.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; @@ -10,7 +8,6 @@ import 'portfolio_display_1.dart'; import 'portfolio_display_2.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; // Import pour les traductions import 'package:realtoken_asset_tracker/utils/parameters.dart'; -import 'package:realtoken_asset_tracker/utils/location_utils.dart'; import 'package:realtoken_asset_tracker/components/filter_widgets.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; @@ -46,17 +43,19 @@ class PortfolioPageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { // Vérifier si les données sont déjà chargées final dataManager = Provider.of(context, listen: false); - + // Si les données du portfolio sont déjà chargées (depuis main.dart) - if (!dataManager.isLoadingMain && dataManager.evmAddresses.isNotEmpty && dataManager.portfolio.isNotEmpty) { + if (!dataManager.isLoadingMain && + dataManager.evmAddresses.isNotEmpty && + dataManager.portfolio.isNotEmpty) { debugPrint("💼 Portfolio: données déjà chargées, skip chargement"); - } + } // Sinon, charger les données avec cache else { debugPrint("💼 Portfolio: chargement des données nécessaire"); await DataFetchUtils.loadDataWithCache(context); } - + // Charger les préférences d'affichage dans tous les cas _loadDisplayPreference(); _loadFilterPreferences(); @@ -100,15 +99,24 @@ class PortfolioPageState extends State { if (mounted) { // Vérifie que le widget est toujours monté setState(() { - _sortOption = prefs.getString('sortOption') ?? S.of(context).sortByInitialLaunchDate; + _sortOption = prefs.getString('sortOption') ?? + S.of(context).sortByInitialLaunchDate; _isAscending = prefs.getBool('isAscending') ?? false; - _selectedCity = prefs.getString('selectedCity')?.isEmpty ?? true ? null : prefs.getString('selectedCity'); - _selectedRegion = prefs.getString('selectedRegion')?.isEmpty ?? true ? null : prefs.getString('selectedRegion'); - _selectedCountry = prefs.getString('selectedCountry')?.isEmpty ?? true ? null : prefs.getString('selectedCountry'); + _selectedCity = prefs.getString('selectedCity')?.isEmpty ?? true + ? null + : prefs.getString('selectedCity'); + _selectedRegion = prefs.getString('selectedRegion')?.isEmpty ?? true + ? null + : prefs.getString('selectedRegion'); + _selectedCountry = prefs.getString('selectedCountry')?.isEmpty ?? true + ? null + : prefs.getString('selectedCountry'); // On récupère l'identifiant interne, par défaut "all" - _rentalStatusFilter = prefs.getString('rentalStatusFilter') ?? rentalStatusAll; + _rentalStatusFilter = + prefs.getString('rentalStatusFilter') ?? rentalStatusAll; // Charger les productTypes sélectionnés - List? savedProductTypes = prefs.getStringList('selectedProductTypes'); + List? savedProductTypes = + prefs.getStringList('selectedProductTypes'); _selectedProductTypes = savedProductTypes?.toSet() ?? {}; }); } @@ -126,7 +134,8 @@ class PortfolioPageState extends State { // On sauvegarde l'identifiant interne await prefs.setString('rentalStatusFilter', _rentalStatusFilter); // Sauvegarder les productTypes sélectionnés - await prefs.setStringList('selectedProductTypes', _selectedProductTypes.toList()); + await prefs.setStringList( + 'selectedProductTypes', _selectedProductTypes.toList()); } // Méthodes de gestion des filtres et tri @@ -171,35 +180,54 @@ class PortfolioPageState extends State { _saveFilterPreferences(); // Sauvegarde } - List> _groupAndSumPortfolio(List> portfolio) { + List> _groupAndSumPortfolio( + List> portfolio) { Map> groupedPortfolio = {}; for (var token in portfolio) { String shortName = token['shortName']; // Utilisez l'identifiant unique double tokenAmount = double.tryParse(token['amount'].toString()) ?? 0.0; - double tokenValue = double.tryParse(token['totalValue'].toString()) ?? 0.0; - double dailyIncome = double.tryParse(token['dailyIncome'].toString()) ?? 0.0; - double monthlyIncome = double.tryParse(token['monthlyIncome'].toString()) ?? 0.0; - double yearlyIncome = double.tryParse(token['yearlyIncome'].toString()) ?? 0.0; + double tokenValue = + double.tryParse(token['totalValue'].toString()) ?? 0.0; + double dailyIncome = + double.tryParse(token['dailyIncome'].toString()) ?? 0.0; + double monthlyIncome = + double.tryParse(token['monthlyIncome'].toString()) ?? 0.0; + double yearlyIncome = + double.tryParse(token['yearlyIncome'].toString()) ?? 0.0; - bool isInWallet = token['source'] == 'wallet'; // Ajout de la vérification pour le wallet - bool isInRMM = token['source'] == 'RMM'; // Ajout de la vérification pour le RMM + bool isInWallet = token['source'] == + 'wallet'; // Ajout de la vérification pour le wallet + bool isInRMM = + token['source'] == 'RMM'; // Ajout de la vérification pour le RMM if (groupedPortfolio.containsKey(shortName)) { - groupedPortfolio[shortName]!['amount'] = (groupedPortfolio[shortName]!['amount'] as double) + tokenAmount; - groupedPortfolio[shortName]!['totalValue'] = (groupedPortfolio[shortName]!['totalValue'] as double) + tokenValue; - groupedPortfolio[shortName]!['dailyIncome'] = (groupedPortfolio[shortName]!['dailyIncome'] as double) + dailyIncome; - groupedPortfolio[shortName]!['monthlyIncome'] = (groupedPortfolio[shortName]!['monthlyIncome'] as double) + monthlyIncome; - groupedPortfolio[shortName]!['yearlyIncome'] = (groupedPortfolio[shortName]!['yearlyIncome'] as double) + yearlyIncome; + groupedPortfolio[shortName]!['amount'] = + (groupedPortfolio[shortName]!['amount'] as double) + tokenAmount; + groupedPortfolio[shortName]!['totalValue'] = + (groupedPortfolio[shortName]!['totalValue'] as double) + tokenValue; + groupedPortfolio[shortName]!['dailyIncome'] = + (groupedPortfolio[shortName]!['dailyIncome'] as double) + + dailyIncome; + groupedPortfolio[shortName]!['monthlyIncome'] = + (groupedPortfolio[shortName]!['monthlyIncome'] as double) + + monthlyIncome; + groupedPortfolio[shortName]!['yearlyIncome'] = + (groupedPortfolio[shortName]!['yearlyIncome'] as double) + + yearlyIncome; groupedPortfolio[shortName]!['inWallet'] |= isInWallet; groupedPortfolio[shortName]!['inRMM'] |= isInRMM; // Agrégation des adresses wallet - if (token['evmAddress'] != null && token['evmAddress'].toString().isNotEmpty) { + if (token['evmAddress'] != null && + token['evmAddress'].toString().isNotEmpty) { if (groupedPortfolio[shortName]!['evmAddresses'] == null) { - groupedPortfolio[shortName]!['evmAddresses'] = {token['evmAddress']}; + groupedPortfolio[shortName]!['evmAddresses'] = { + token['evmAddress'] + }; } else { - (groupedPortfolio[shortName]!['evmAddresses'] as Set).add(token['evmAddress']); + (groupedPortfolio[shortName]!['evmAddresses'] as Set) + .add(token['evmAddress']); } } } else { @@ -212,7 +240,11 @@ class PortfolioPageState extends State { groupedPortfolio[shortName]!['inWallet'] = isInWallet; groupedPortfolio[shortName]!['inRMM'] = isInRMM; // Initialisation du set d'adresses wallet - groupedPortfolio[shortName]!['evmAddresses'] = (token['evmAddress'] != null && token['evmAddress'].toString().isNotEmpty) ? {token['evmAddress']} : {}; + groupedPortfolio[shortName]!['evmAddresses'] = + (token['evmAddress'] != null && + token['evmAddress'].toString().isNotEmpty) + ? {token['evmAddress']} + : {}; } } @@ -220,23 +252,32 @@ class PortfolioPageState extends State { } // Modifier la méthode pour appliquer le filtre sur le statut de location - List> _filterAndSortPortfolio(List> portfolio) { + List> _filterAndSortPortfolio( + List> portfolio) { // Regroupez et cumulez les tokens similaires - List> groupedPortfolio = _groupAndSumPortfolio(portfolio); + List> groupedPortfolio = + _groupAndSumPortfolio(portfolio); // Filtrez sur différents critères List> filteredPortfolio = groupedPortfolio .where((token) => // Filtre par recherche - token['fullName'].toLowerCase().contains(_searchQuery.toLowerCase()) && + token['fullName'] + .toLowerCase() + .contains(_searchQuery.toLowerCase()) && // Filtre par ville (conservé pour compatibilité) - (_selectedCity == null || token['fullName'].contains(_selectedCity!)) && + (_selectedCity == null || + token['fullName'].contains(_selectedCity!)) && // Filtre par région - (_selectedRegion == null || (token['regionCode'] != null && token['regionCode'] == _selectedRegion)) && + (_selectedRegion == null || + (token['regionCode'] != null && + token['regionCode'] == _selectedRegion)) && // Filtre par pays - (_selectedCountry == null || _matchesCountryFilter(token, _selectedCountry)) && + (_selectedCountry == null || + _matchesCountryFilter(token, _selectedCountry)) && // Filtre par statut de location - (_rentalStatusFilter == rentalStatusAll || _filterByRentalStatus(token))) + (_rentalStatusFilter == rentalStatusAll || + _filterByRentalStatus(token))) .toList(); // Filtre par wallet (si au moins un wallet est sélectionné) @@ -248,7 +289,8 @@ class PortfolioPageState extends State { // Si c'est une liste, la convertir en Set if (token['wallets'] is List) { var walletsList = token['wallets'] as List; - Set tokenWallets = walletsList.map((w) => w.toString()).toSet(); + Set tokenWallets = + walletsList.map((w) => w.toString()).toSet(); // Vérifier s'il y a une intersection avec les wallets sélectionnés return tokenWallets.intersection(_selectedWallets).isNotEmpty; } @@ -270,7 +312,8 @@ class PortfolioPageState extends State { if (_selectedTokenTypes.isEmpty) { // Aucun type sélectionné : ne retourner aucun token filteredPortfolio = []; - } else if (!(_selectedTokenTypes.contains("wallet") && _selectedTokenTypes.contains("RMM"))) { + } else if (!(_selectedTokenTypes.contains("wallet") && + _selectedTokenTypes.contains("RMM"))) { // Si la sélection n'inclut pas tous les types, appliquer le filtre par token["source"] filteredPortfolio = filteredPortfolio.where((token) { String tokenType = token['source'] ?? ''; @@ -288,16 +331,28 @@ class PortfolioPageState extends State { // Tri en fonction des options sélectionnées if (_sortOption == S.of(context).sortByName) { - filteredPortfolio.sort((a, b) => _isAscending ? a['shortName'].compareTo(b['shortName']) : b['shortName'].compareTo(a['shortName'])); + filteredPortfolio.sort((a, b) => _isAscending + ? a['shortName'].compareTo(b['shortName']) + : b['shortName'].compareTo(a['shortName'])); } else if (_sortOption == S.of(context).sortByValue) { - filteredPortfolio.sort((a, b) => _isAscending ? a['totalValue'].compareTo(b['totalValue']) : b['totalValue'].compareTo(a['totalValue'])); + filteredPortfolio.sort((a, b) => _isAscending + ? a['totalValue'].compareTo(b['totalValue']) + : b['totalValue'].compareTo(a['totalValue'])); } else if (_sortOption == S.of(context).sortByAPY) { - filteredPortfolio.sort((a, b) => _isAscending ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); + filteredPortfolio.sort((a, b) => _isAscending + ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) + : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); } else if (_sortOption == S.of(context).sortByInitialLaunchDate) { filteredPortfolio.sort((a, b) { - final dateA = a['initialLaunchDate'] != null ? DateTime.tryParse(a['initialLaunchDate']) : DateTime(1970); - final dateB = b['initialLaunchDate'] != null ? DateTime.tryParse(b['initialLaunchDate']) : DateTime(1970); - return _isAscending ? dateA!.compareTo(dateB!) : dateB!.compareTo(dateA!); + final dateA = a['initialLaunchDate'] != null + ? DateTime.tryParse(a['initialLaunchDate']) + : DateTime(1970); + final dateB = b['initialLaunchDate'] != null + ? DateTime.tryParse(b['initialLaunchDate']) + : DateTime(1970); + return _isAscending + ? dateA!.compareTo(dateB!) + : dateB!.compareTo(dateA!); }); } @@ -320,26 +375,31 @@ class PortfolioPageState extends State { } // Méthodes factorisées utilisant FilterWidgets - List _getUniqueCities(List> portfolio) => FilterWidgets.getUniqueCities(portfolio); - List _getUniqueRegions(List> portfolio) => FilterWidgets.getUniqueRegions(portfolio); - List _getUniqueCountries(List> portfolio) => FilterWidgets.getUniqueCountries(portfolio); - + List _getUniqueCities(List> portfolio) => + FilterWidgets.getUniqueCities(portfolio); + List _getUniqueRegions(List> portfolio) => + FilterWidgets.getUniqueRegions(portfolio); + List _getUniqueCountries(List> portfolio) => + FilterWidgets.getUniqueCountries(portfolio); + // Méthode pour vérifier si un token correspond au filtre pays - bool _matchesCountryFilter(Map token, String? selectedCountry) { + bool _matchesCountryFilter( + Map token, String? selectedCountry) { if (selectedCountry == null) return true; - + String tokenCountry = token['country'] ?? "Unknown Country"; - + // Si "Series XX" est sélectionné, filtrer tous les tokens factoring_profitshare avec des séries if (selectedCountry == "Series XX") { - return (token['productType']?.toString().toLowerCase() == 'factoring_profitshare') && - tokenCountry.toLowerCase().startsWith('series '); + return (token['productType']?.toString().toLowerCase() == + 'factoring_profitshare') && + tokenCountry.toLowerCase().startsWith('series '); } - + // Filtre normal return tokenCountry == selectedCountry; } - + // Méthode pour obtenir les types de produits uniques List _getUniqueProductTypes(List> portfolio) { Set productTypes = {}; @@ -380,21 +440,25 @@ class PortfolioPageState extends State { extendBodyBehindAppBar: false, body: Consumer( builder: (context, dataManager, child) { - final sortedFilteredPortfolio = _filterAndSortPortfolio(dataManager.portfolio); + final sortedFilteredPortfolio = + _filterAndSortPortfolio(dataManager.portfolio); final uniqueCities = _getUniqueCities(dataManager.portfolio); final uniqueRegions = _getUniqueRegions(dataManager.portfolio); final uniqueCountries = _getUniqueCountries(dataManager.portfolio); return Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top + kToolbarHeight), + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + kToolbarHeight), child: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( floating: true, snap: true, automaticallyImplyLeading: false, - expandedHeight: UIUtils.getSliverAppBarHeight(context) + 65, + expandedHeight: + UIUtils.getSliverAppBarHeight(context) + 65, flexibleSpace: FlexibleSpaceBar( background: Container( color: Theme.of(context).scaffoldBackgroundColor, @@ -403,7 +467,8 @@ class PortfolioPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 2.0), child: Column( children: [ // Barre avec recherche et bouton d'affichage @@ -412,13 +477,17 @@ class PortfolioPageState extends State { // Conteneur de recherche avec bords arrondis Expanded( child: Container( - margin: const EdgeInsets.only(bottom: 8, right: 8), + margin: const EdgeInsets.only( + bottom: 8, right: 8), decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(24), + color: + Theme.of(context).cardColor, + borderRadius: + BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black + .withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2), ), @@ -428,10 +497,13 @@ class PortfolioPageState extends State { children: [ // Icône de recherche Padding( - padding: const EdgeInsets.only(left: 12.0), + padding: + const EdgeInsets.only( + left: 12.0), child: Icon( Icons.search, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, size: 18, ), ), @@ -442,16 +514,36 @@ class PortfolioPageState extends State { onChanged: (value) { _updateSearchQuery(value); }, - style: TextStyle(fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset()), + style: TextStyle( + fontSize: 14 + + Provider.of( + context, + listen: + false) + .getTextSizeOffset()), decoration: InputDecoration( isDense: true, - hintText: S.of(context).searchHint, + hintText: S + .of(context) + .searchHint, hintStyle: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), - color: Theme.of(context).textTheme.bodySmall?.color, + fontSize: 14 + + Provider.of( + context, + listen: + false) + .getTextSizeOffset(), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + contentPadding: + const EdgeInsets + .symmetric( + vertical: 8, + horizontal: 12), ), ), ), @@ -462,11 +554,16 @@ class PortfolioPageState extends State { // Bouton d'affichage séparé Container( - margin: const EdgeInsets.only(bottom: 8), + margin: + const EdgeInsets.only(bottom: 8), padding: EdgeInsets.zero, child: _buildFilterButton( - icon: _isDisplay1 ? Icons.view_module : Icons.view_list, - label: _isDisplay1 ? S.of(context).gridView : S.of(context).listView, + icon: _isDisplay1 + ? Icons.view_module + : Icons.view_list, + label: _isDisplay1 + ? S.of(context).gridView + : S.of(context).listView, onTap: _toggleDisplay, ), ), @@ -483,8 +580,10 @@ class PortfolioPageState extends State { context: context, icon: Icons.flag, selectedCountry: _selectedCountry, - onCountryChanged: (newSelectedCountry) { - _updateCountryFilter(newSelectedCountry); + onCountryChanged: + (newSelectedCountry) { + _updateCountryFilter( + newSelectedCountry); }, ), @@ -495,8 +594,10 @@ class PortfolioPageState extends State { context: context, icon: Icons.map, selectedRegion: _selectedRegion, - onRegionChanged: (newSelectedRegion) { - _updateRegionFilter(newSelectedRegion); + onRegionChanged: + (newSelectedRegion) { + _updateRegionFilter( + newSelectedRegion); }, ), @@ -506,10 +607,13 @@ class PortfolioPageState extends State { _buildProductTypeFilterMenu( context: context, icon: Icons.category, - selectedProductTypes: _selectedProductTypes, - onProductTypesChanged: (newSelectedProductTypes) { + selectedProductTypes: + _selectedProductTypes, + onProductTypesChanged: + (newSelectedProductTypes) { setState(() { - _selectedProductTypes = newSelectedProductTypes; + _selectedProductTypes = + newSelectedProductTypes; }); _saveFilterPreferences(); }, @@ -521,9 +625,12 @@ class PortfolioPageState extends State { _buildRentalStatusFilterMenu( context: context, icon: Icons.home_work, - selectedRentalStatus: _rentalStatusFilter, - onRentalStatusChanged: (newRentalStatus) { - _updateRentalStatusFilter(newRentalStatus); + selectedRentalStatus: + _rentalStatusFilter, + onRentalStatusChanged: + (newRentalStatus) { + _updateRentalStatusFilter( + newRentalStatus); _saveFilterPreferences(); }, ), @@ -534,10 +641,13 @@ class PortfolioPageState extends State { _buildTokenTypeFilterMenu( context: context, icon: Icons.account_tree, - selectedTokenTypes: _selectedTokenTypes, - onTokenTypesChanged: (newSelectedTokenTypes) { + selectedTokenTypes: + _selectedTokenTypes, + onTokenTypesChanged: + (newSelectedTokenTypes) { setState(() { - _selectedTokenTypes = newSelectedTokenTypes; + _selectedTokenTypes = + newSelectedTokenTypes; }); _saveFilterPreferences(); }, @@ -550,9 +660,11 @@ class PortfolioPageState extends State { context: context, icon: Icons.account_balance_wallet, selectedWallets: _selectedWallets, - onWalletsChanged: (newSelectedWallets) { + onWalletsChanged: + (newSelectedWallets) { setState(() { - _selectedWallets = newSelectedWallets; + _selectedWallets = + newSelectedWallets; }); }, ), @@ -562,13 +674,16 @@ class PortfolioPageState extends State { //Tri Container( - margin: EdgeInsets.zero, // Sans marge + margin: + EdgeInsets.zero, // Sans marge child: PopupMenuButton( tooltip: "", onSelected: (String value) { - if (value == 'asc' || value == 'desc') { + if (value == 'asc' || + value == 'desc') { setState(() { - _isAscending = (value == 'asc'); + _isAscending = + (value == 'asc'); }); _saveFilterPreferences(); // Sauvegarder après la modification } else { @@ -577,53 +692,80 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context) + .cardColor + .withValues(alpha: 0.97), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: + BorderRadius.circular(16), ), child: Container( - padding: const EdgeInsets.all(8), + padding: + const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(12), ), child: Icon( Icons.sort, size: 20, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), itemBuilder: (context) => [ CheckedPopupMenuItem( - value: S.of(context).sortByName, - checked: _sortOption == S.of(context).sortByName, - child: Text(S.of(context).sortByName), + value: + S.of(context).sortByName, + checked: _sortOption == + S.of(context).sortByName, + child: Text( + S.of(context).sortByName), ), CheckedPopupMenuItem( - value: S.of(context).sortByValue, - checked: _sortOption == S.of(context).sortByValue, - child: Text(S.of(context).sortByValue), + value: + S.of(context).sortByValue, + checked: _sortOption == + S.of(context).sortByValue, + child: Text(S + .of(context) + .sortByValue), ), CheckedPopupMenuItem( - value: S.of(context).sortByAPY, - checked: _sortOption == S.of(context).sortByAPY, - child: Text(S.of(context).sortByAPY), + value: + S.of(context).sortByAPY, + checked: _sortOption == + S.of(context).sortByAPY, + child: Text( + S.of(context).sortByAPY), ), CheckedPopupMenuItem( - value: S.of(context).sortByInitialLaunchDate, - checked: _sortOption == S.of(context).sortByInitialLaunchDate, - child: Text(S.of(context).sortByInitialLaunchDate), + value: S + .of(context) + .sortByInitialLaunchDate, + checked: _sortOption == + S + .of(context) + .sortByInitialLaunchDate, + child: Text(S + .of(context) + .sortByInitialLaunchDate), ), const PopupMenuDivider(), CheckedPopupMenuItem( value: 'asc', checked: _isAscending, - child: Text(S.of(context).ascending), + child: Text( + S.of(context).ascending), ), CheckedPopupMenuItem( value: 'desc', checked: !_isAscending, - child: Text(S.of(context).descending), + child: Text( + S.of(context).descending), ), ], ), @@ -641,15 +783,20 @@ class PortfolioPageState extends State { ) ]; }, - body: _isDisplay1 - ? PortfolioDisplay1( - portfolio: sortedFilteredPortfolio, - isLoading: Provider.of(context).isLoadingMain || Provider.of(context).isUpdatingData, - ) - : PortfolioDisplay2( - portfolio: sortedFilteredPortfolio, - isLoading: Provider.of(context).isLoadingMain || Provider.of(context).isUpdatingData, - ), ), + body: _isDisplay1 + ? PortfolioDisplay1( + portfolio: sortedFilteredPortfolio, + isLoading: Provider.of(context) + .isLoadingMain || + Provider.of(context).isUpdatingData, + ) + : PortfolioDisplay2( + portfolio: sortedFilteredPortfolio, + isLoading: Provider.of(context) + .isLoadingMain || + Provider.of(context).isUpdatingData, + ), + ), ); }, ), @@ -663,12 +810,13 @@ class PortfolioPageState extends State { required IconData icon, required String label, required VoidCallback onTap, - }) => FilterWidgets.buildFilterButton( - context: context, - icon: icon, - label: label, - onTap: onTap, - ); + }) => + FilterWidgets.buildFilterButton( + context: context, + icon: icon, + label: label, + onTap: onTap, + ); Widget _buildFilterPopupMenu({ required BuildContext context, @@ -676,13 +824,14 @@ class PortfolioPageState extends State { required String label, required List> items, required Function(String) onSelected, - }) => FilterWidgets.buildFilterPopupMenu( - context: context, - icon: icon, - label: label, - items: items, - onSelected: onSelected, - ); + }) => + FilterWidgets.buildFilterPopupMenu( + context: context, + icon: icon, + label: label, + items: items, + onSelected: onSelected, + ); // Helper pour obtenir le label du tri actuel String _getSortLabel(BuildContext context) { @@ -724,15 +873,18 @@ class PortfolioPageState extends State { activeFilters.add("NR"); } } - if (!(_selectedTokenTypes.contains("wallet") && _selectedTokenTypes.contains("RMM"))) { - if (_selectedTokenTypes.contains("wallet") && !_selectedTokenTypes.contains("RMM")) { + if (!(_selectedTokenTypes.contains("wallet") && + _selectedTokenTypes.contains("RMM"))) { + if (_selectedTokenTypes.contains("wallet") && + !_selectedTokenTypes.contains("RMM")) { activeFilters.add("W"); - } else if (!_selectedTokenTypes.contains("wallet") && _selectedTokenTypes.contains("RMM")) { + } else if (!_selectedTokenTypes.contains("wallet") && + _selectedTokenTypes.contains("RMM")) { activeFilters.add("RMM"); } } if (activeFilters.isNotEmpty) { - return "${activeFilters.join('+')}"; + return activeFilters.join('+'); } return baseLabel; } @@ -756,20 +908,20 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: selectedWallets.isNotEmpty - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: selectedWallets.isNotEmpty + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: selectedWallets.isNotEmpty - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: selectedWallets.isNotEmpty + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -793,34 +945,44 @@ class PortfolioPageState extends State { ), ), const PopupMenuDivider(), - ...Provider.of(context, listen: false).evmAddresses.toSet().toList().map((wallet) => PopupMenuItem( - value: wallet, - child: StatefulBuilder( - builder: (context, setStateLocal) { - return InkWell( - onTap: () { - setState(() { - if (selectedWallets.contains(wallet)) { - selectedWallets.remove(wallet); - } else { - selectedWallets.add(wallet); - } - }); - setStateLocal(() {}); + ...Provider.of(context, listen: false) + .evmAddresses + .toSet() + .toList() + .map((wallet) => PopupMenuItem( + value: wallet, + child: StatefulBuilder( + builder: (context, setStateLocal) { + return InkWell( + onTap: () { + setState(() { + if (selectedWallets.contains(wallet)) { + selectedWallets.remove(wallet); + } else { + selectedWallets.add(wallet); + } + }); + setStateLocal(() {}); + }, + child: Row( + children: [ + selectedWallets.contains(wallet) + ? const Icon(Icons.check, size: 20) + : const SizedBox(width: 20), + const SizedBox(width: 8.0), + const Icon(Icons.account_balance_wallet, + size: 20), + const SizedBox(width: 8.0), + Flexible( + child: Text(wallet.length > 15 + ? '${wallet.substring(0, 6)}...${wallet.substring(wallet.length - 4)}' + : wallet)), + ], + ), + ); }, - child: Row( - children: [ - selectedWallets.contains(wallet) ? const Icon(Icons.check, size: 20) : const SizedBox(width: 20), - const SizedBox(width: 8.0), - const Icon(Icons.account_balance_wallet, size: 20), - const SizedBox(width: 8.0), - Flexible(child: Text(wallet.length > 15 ? '${wallet.substring(0, 6)}...${wallet.substring(wallet.length - 4)}' : wallet)), - ], - ), - ); - }, - ), - )), + ), + )), // Bouton pour fermer/appliquer const PopupMenuDivider(), PopupMenuItem( @@ -854,108 +1016,110 @@ class PortfolioPageState extends State { required Function(Set) onProductTypesChanged, }) { return PopupMenuButton( - tooltip: "", - onSelected: (String value) { - // La logique de sélection est gérée dans StatefulBuilder - }, - offset: const Offset(0, 40), - elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: selectedProductTypes.isNotEmpty - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: selectedProductTypes.isNotEmpty + tooltip: "", + onSelected: (String value) { + // La logique de sélection est gérée dans StatefulBuilder + }, + offset: const Offset(0, 40), + elevation: 8, + color: Theme.of(context).cardColor.withValues(alpha: 0.97), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: selectedProductTypes.isNotEmpty + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: selectedProductTypes.isNotEmpty ? Border.all(color: Theme.of(context).primaryColor, width: 2) : null, - ), - child: Icon( - icon, - size: 20, - color: Theme.of(context).primaryColor, - ), ), - itemBuilder: (context) { - final uniqueProductTypes = _getUniqueProductTypes(Provider.of(context, listen: false).portfolio); - - return [ - PopupMenuItem( - value: "product_type_header", - enabled: false, - child: Row( - children: const [ - Icon(Icons.category, size: 20), - SizedBox(width: 8.0), - Text( - "Types de produit", - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), + child: Icon( + icon, + size: 20, + color: Theme.of(context).primaryColor, + ), + ), + itemBuilder: (context) { + final uniqueProductTypes = _getUniqueProductTypes( + Provider.of(context, listen: false).portfolio); + + return [ + PopupMenuItem( + value: "product_type_header", + enabled: false, + child: Row( + children: const [ + Icon(Icons.category, size: 20), + SizedBox(width: 8.0), + Text( + "Types de produit", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], ), - const PopupMenuDivider(), - ...uniqueProductTypes.map((productType) => PopupMenuItem( - value: productType, - child: StatefulBuilder( - builder: (context, setStateLocal) { - return InkWell( - onTap: () { - setState(() { - if (selectedProductTypes.contains(productType)) { - selectedProductTypes.remove(productType); - } else { - selectedProductTypes.add(productType); - } - }); - setStateLocal(() {}); - onProductTypesChanged(selectedProductTypes); - }, - child: Row( - children: [ - selectedProductTypes.contains(productType) - ? const Icon(Icons.check, size: 20) + ), + const PopupMenuDivider(), + ...uniqueProductTypes.map((productType) => PopupMenuItem( + value: productType, + child: StatefulBuilder( + builder: (context, setStateLocal) { + return InkWell( + onTap: () { + setState(() { + if (selectedProductTypes.contains(productType)) { + selectedProductTypes.remove(productType); + } else { + selectedProductTypes.add(productType); + } + }); + setStateLocal(() {}); + onProductTypesChanged(selectedProductTypes); + }, + child: Row( + children: [ + selectedProductTypes.contains(productType) + ? const Icon(Icons.check, size: 20) : const SizedBox(width: 20), - const SizedBox(width: 8.0), - Icon(_getProductTypeIcon(productType), size: 20), - const SizedBox(width: 8.0), - Flexible( - child: Text(_getLocalizedProductTypeName(context, productType)), - ), - ], - ), - ); - }, - ), - )), - // Bouton pour fermer/appliquer - const PopupMenuDivider(), - PopupMenuItem( - value: "apply_product_types", - child: Center( - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); + const SizedBox(width: 8.0), + Icon(_getProductTypeIcon(productType), size: 20), + const SizedBox(width: 8.0), + Flexible( + child: Text(_getLocalizedProductTypeName( + context, productType)), + ), + ], + ), + ); }, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + ), + )), + // Bouton pour fermer/appliquer + const PopupMenuDivider(), + PopupMenuItem( + value: "apply_product_types", + child: Center( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - child: Text("Appliquer"), ), + child: Text("Appliquer"), ), ), - ]; - }, - ); + ), + ]; + }, + ); } // Filtre Region @@ -978,20 +1142,20 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: selectedRegion != null - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: selectedRegion != null + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: selectedRegion != null - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: selectedRegion != null + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -1000,8 +1164,9 @@ class PortfolioPageState extends State { ), ), itemBuilder: (context) { - final uniqueRegions = _getUniqueRegions(Provider.of(context, listen: false).portfolio); - + final uniqueRegions = _getUniqueRegions( + Provider.of(context, listen: false).portfolio); + return [ PopupMenuItem( value: "region_header", @@ -1022,7 +1187,9 @@ class PortfolioPageState extends State { value: "all_regions", child: Row( children: [ - selectedRegion == null ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRegion == null + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.public, size: 20), SizedBox(width: 8.0), @@ -1035,7 +1202,9 @@ class PortfolioPageState extends State { value: region, child: Row( children: [ - selectedRegion == region ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRegion == region + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), if (region != "Unknown Region") Padding( @@ -1044,10 +1213,12 @@ class PortfolioPageState extends State { 'assets/states/${region.toLowerCase()}.png', width: 24, height: 16, - errorBuilder: (context, _, __) => const Icon(Icons.flag, size: 20), + errorBuilder: (context, _, __) => + const Icon(Icons.flag, size: 20), ), ), - Flexible(child: Text(Parameters.getRegionDisplayName(region))), + Flexible( + child: Text(Parameters.getRegionDisplayName(region))), ], ), )), @@ -1077,20 +1248,20 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: selectedCountry != null - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: selectedCountry != null + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: selectedCountry != null - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: selectedCountry != null + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -1099,8 +1270,9 @@ class PortfolioPageState extends State { ), ), itemBuilder: (context) { - final uniqueCountries = _getUniqueCountries(Provider.of(context, listen: false).portfolio); - + final uniqueCountries = _getUniqueCountries( + Provider.of(context, listen: false).portfolio); + return [ PopupMenuItem( value: "country_header", @@ -1121,7 +1293,9 @@ class PortfolioPageState extends State { value: "all_countries", child: Row( children: [ - selectedCountry == null ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedCountry == null + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.public, size: 20), SizedBox(width: 8.0), @@ -1134,7 +1308,9 @@ class PortfolioPageState extends State { value: country, child: Row( children: [ - selectedCountry == country ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedCountry == country + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), if (country != "Unknown Country") Padding( @@ -1143,7 +1319,8 @@ class PortfolioPageState extends State { 'assets/country/${Parameters.getCountryFileName(country)}.png', width: 24, height: 16, - errorBuilder: (context, _, __) => const Icon(Icons.flag, size: 20), + errorBuilder: (context, _, __) => + const Icon(Icons.flag, size: 20), ), ), Flexible(child: Text(country)), @@ -1172,20 +1349,20 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: selectedRentalStatus != rentalStatusAll - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: selectedRentalStatus != rentalStatusAll + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: selectedRentalStatus != rentalStatusAll - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: selectedRentalStatus != rentalStatusAll + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -1213,7 +1390,9 @@ class PortfolioPageState extends State { value: rentalStatusAll, child: Row( children: [ - selectedRentalStatus == rentalStatusAll ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRentalStatus == rentalStatusAll + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.home_work_outlined, size: 20), SizedBox(width: 8.0), @@ -1225,7 +1404,9 @@ class PortfolioPageState extends State { value: rentalStatusRented, child: Row( children: [ - selectedRentalStatus == rentalStatusRented ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRentalStatus == rentalStatusRented + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.check_circle, size: 20, color: Colors.green), SizedBox(width: 8.0), @@ -1237,7 +1418,9 @@ class PortfolioPageState extends State { value: rentalStatusPartially, child: Row( children: [ - selectedRentalStatus == rentalStatusPartially ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRentalStatus == rentalStatusPartially + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.adjust, size: 20, color: Colors.orange), SizedBox(width: 8.0), @@ -1249,7 +1432,9 @@ class PortfolioPageState extends State { value: rentalStatusNotRented, child: Row( children: [ - selectedRentalStatus == rentalStatusNotRented ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedRentalStatus == rentalStatusNotRented + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.cancel, size: 20, color: Colors.red), SizedBox(width: 8.0), @@ -1278,20 +1463,20 @@ class PortfolioPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: selectedTokenTypes.isNotEmpty - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: selectedTokenTypes.isNotEmpty + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: selectedTokenTypes.isNotEmpty - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: selectedTokenTypes.isNotEmpty + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -1333,7 +1518,9 @@ class PortfolioPageState extends State { }, child: Row( children: [ - selectedTokenTypes.contains("wallet") ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedTokenTypes.contains("wallet") + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.account_balance_wallet, size: 20), SizedBox(width: 8.0), @@ -1362,7 +1549,9 @@ class PortfolioPageState extends State { }, child: Row( children: [ - selectedTokenTypes.contains("RMM") ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + selectedTokenTypes.contains("RMM") + ? Icon(Icons.check, size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), Icon(Icons.business, size: 20), SizedBox(width: 8.0), @@ -1379,7 +1568,8 @@ class PortfolioPageState extends State { } // Méthode pour obtenir le nom localisé du type de produit - String _getLocalizedProductTypeName(BuildContext context, String productType) { + String _getLocalizedProductTypeName( + BuildContext context, String productType) { switch (productType.toLowerCase()) { case 'real_estate_rental': return S.of(context).productTypeRealEstateRental; diff --git a/lib/pages/propertiesForSale/PropertiesForSaleRealt.dart b/lib/pages/propertiesForSale/PropertiesForSaleRealt.dart index 0928cf4..f360c05 100644 --- a/lib/pages/propertiesForSale/PropertiesForSaleRealt.dart +++ b/lib/pages/propertiesForSale/PropertiesForSaleRealt.dart @@ -91,13 +91,16 @@ class _PropertiesForSaleRealtState extends State { child: Text( S.of(context).noPropertiesForSale, style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, ), ), ) : AlignedGridView.count( - padding: const EdgeInsets.only(top: 16, bottom: 80, left: 16, right: 16), + padding: const EdgeInsets.only( + top: 16, bottom: 80, left: 16, right: 16), crossAxisCount: MediaQuery.of(context).size.width > 700 ? 2 : 1, mainAxisSpacing: 16, crossAxisSpacing: 16, @@ -105,20 +108,31 @@ class _PropertiesForSaleRealtState extends State { itemBuilder: (context, index) { final property = propertiesForSale[index]; - final imageUrl = (property['imageLink'] != null && property['imageLink'] is List && property['imageLink'].isNotEmpty) ? property['imageLink'][0] : ''; - final title = property['shortName'] ?? S.of(context).nameUnavailable; - final double stock = (property['stock'] as num?)?.toDouble() ?? 0.0; + final imageUrl = (property['imageLink'] != null && + property['imageLink'] is List && + property['imageLink'].isNotEmpty) + ? property['imageLink'][0] + : ''; + final title = + property['shortName'] ?? S.of(context).nameUnavailable; + final double stock = + (property['stock'] as num?)?.toDouble() ?? 0.0; - final double tokenPrice = (property['tokenPrice'] as num?)?.toDouble() ?? 0.0; - final double annualPercentageYield = (property['annualPercentageYield'] as num?)?.toDouble() ?? 0.0; + final double tokenPrice = + (property['tokenPrice'] as num?)?.toDouble() ?? 0.0; + final double annualPercentageYield = + (property['annualPercentageYield'] as num?)?.toDouble() ?? + 0.0; - final double totalTokens = (property['totalTokens'] as num?)?.toDouble() ?? 0.0; + final double totalTokens = + (property['totalTokens'] as num?)?.toDouble() ?? 0.0; final country = property['country'] ?? 'unknown'; final city = property['city'] ?? 'unknown'; final status = property['status'] ?? 'Unknown'; - final sellPercentage = (totalTokens - stock) / totalTokens * 100; + final sellPercentage = + (totalTokens - stock) / totalTokens * 100; return GestureDetector( onTap: () { @@ -130,7 +144,9 @@ class _PropertiesForSaleRealtState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -153,16 +169,22 @@ class _PropertiesForSaleRealtState extends State { : CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, - errorWidget: (context, url, error) => const Icon(CupertinoIcons.photo, color: CupertinoColors.systemGrey), + errorWidget: (context, url, error) => + const Icon(CupertinoIcons.photo, + color: + CupertinoColors.systemGrey), ), ), Positioned( top: 12, right: 12, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: status == 'Available' ? CupertinoColors.activeGreen : CupertinoColors.systemRed, + color: status == 'Available' + ? CupertinoColors.activeGreen + : CupertinoColors.systemRed, borderRadius: BorderRadius.circular(20), ), child: Text( @@ -186,26 +208,41 @@ class _PropertiesForSaleRealtState extends State { children: [ if (country != 'unknown') Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: + const EdgeInsets.only(right: 8.0), child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), + borderRadius: + BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), + color: Theme.of(context) + .brightness == + Brightness.dark + ? Colors.black + .withValues(alpha: 0.2) + : Colors.black.withValues( + alpha: 0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: + BorderRadius.circular(4), child: Image.asset( 'assets/country/${Parameters.getCountryFileName(country)}.png', width: 24, height: 24, - errorBuilder: (context, error, stackTrace) { - return Icon(CupertinoIcons.flag, size: 24, color: Theme.of(context).textTheme.bodyMedium?.color); + errorBuilder: + (context, error, stackTrace) { + return Icon(CupertinoIcons.flag, + size: 24, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color); }, ), ), @@ -217,7 +254,10 @@ class _PropertiesForSaleRealtState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, letterSpacing: -0.5, ), maxLines: 1, @@ -232,14 +272,20 @@ class _PropertiesForSaleRealtState extends State { Icon( CupertinoIcons.location_solid, size: 14, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), const SizedBox(width: 4), Text( city, style: TextStyle( fontSize: 14, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, fontWeight: FontWeight.w500, ), ), @@ -249,17 +295,22 @@ class _PropertiesForSaleRealtState extends State { _buildGaugeForRent(sellPercentage, context), const SizedBox(height: 12), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( 'Stock', style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, fontWeight: FontWeight.w500, ), ), @@ -268,7 +319,10 @@ class _PropertiesForSaleRealtState extends State { '${stock.toInt()} / ${totalTokens.toInt()}', style: TextStyle( fontSize: 14, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, fontWeight: FontWeight.w600, ), ), @@ -277,22 +331,31 @@ class _PropertiesForSaleRealtState extends State { ), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( 'Prix', style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( - currencyUtils.formatCurrency(tokenPrice, currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + tokenPrice, + currencyUtils.currencySymbol), style: TextStyle( fontSize: 14, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, fontWeight: FontWeight.w600, ), ), @@ -301,13 +364,17 @@ class _PropertiesForSaleRealtState extends State { ), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( 'Rendement', style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, fontWeight: FontWeight.w500, ), ), @@ -316,7 +383,8 @@ class _PropertiesForSaleRealtState extends State { '${annualPercentageYield.toStringAsFixed(2)}%', style: TextStyle( fontSize: 14, - color: CupertinoColors.activeGreen, + color: + CupertinoColors.activeGreen, fontWeight: FontWeight.w600, ), ), @@ -328,10 +396,12 @@ class _PropertiesForSaleRealtState extends State { const SizedBox(height: 16), CupertinoButton( padding: EdgeInsets.zero, - onPressed: () => UrlUtils.launchURL(property['marketplaceLink']), + onPressed: () => UrlUtils.launchURL( + property['marketplaceLink']), child: Container( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric( + vertical: 12), decoration: BoxDecoration( color: CupertinoColors.activeBlue, borderRadius: BorderRadius.circular(10), diff --git a/lib/pages/propertiesForSale/PropertiesForSaleSecondary.dart b/lib/pages/propertiesForSale/PropertiesForSaleSecondary.dart index aafd4d8..952978d 100644 --- a/lib/pages/propertiesForSale/PropertiesForSaleSecondary.dart +++ b/lib/pages/propertiesForSale/PropertiesForSaleSecondary.dart @@ -1,7 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'dart:ui'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:hive/hive.dart'; import 'package:provider/provider.dart'; @@ -19,10 +18,12 @@ class PropertiesForSaleSecondary extends StatefulWidget { const PropertiesForSaleSecondary({super.key}); @override - _PropertiesForSaleSecondaryState createState() => _PropertiesForSaleSecondaryState(); + _PropertiesForSaleSecondaryState createState() => + _PropertiesForSaleSecondaryState(); } -class _PropertiesForSaleSecondaryState extends State { +class _PropertiesForSaleSecondaryState + extends State { String? lastUpdateTime; bool isSearching = false; String searchQuery = ''; @@ -75,13 +76,19 @@ class _PropertiesForSaleSecondaryState extends State final groupedOffers = >>{}; for (var offer in dataManager.yamMarket) { // Filtrer par type d'offre - if (selectedOfferType == "vente" && offer['token_to_buy'] != null) continue; - if (selectedOfferType == "achat" && offer['token_to_buy'] == null) continue; + if (selectedOfferType == "vente" && offer['token_to_buy'] != null) + continue; + if (selectedOfferType == "achat" && offer['token_to_buy'] == null) + continue; // Si on veut cacher les offres non whitelistées, on vérifie if (hideNonWhitelisted) { - final String? tokenIdentifier = offer['token_to_sell'] ?? offer['token_to_buy']; - final bool isOfferWhitelisted = dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == tokenIdentifier?.toLowerCase()); + final String? tokenIdentifier = + offer['token_to_sell'] ?? offer['token_to_buy']; + final bool isOfferWhitelisted = dataManager.whitelistTokens.any( + (whitelisted) => + whitelisted['token'].toLowerCase() == + tokenIdentifier?.toLowerCase()); if (!isOfferWhitelisted) continue; } @@ -105,7 +112,9 @@ class _PropertiesForSaleSecondaryState extends State } else { final deltaA = ((a['token_value'] / a['token_price'] - 1) * 100); final deltaB = ((b['token_value'] / b['token_price'] - 1) * 100); - return ascending ? deltaA.compareTo(deltaB) : deltaB.compareTo(deltaA); + return ascending + ? deltaA.compareTo(deltaB) + : deltaB.compareTo(deltaA); } }); }); @@ -120,7 +129,8 @@ class _PropertiesForSaleSecondaryState extends State children: [ // Barre de recherche et affichage de la dernière mise à jour dans un style iOS Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -148,9 +158,11 @@ class _PropertiesForSaleSecondaryState extends State controller: searchController, decoration: InputDecoration( hintText: S.of(context).search_hint, - prefixIcon: Icon(Icons.search, color: Colors.grey, size: 20), + prefixIcon: Icon(Icons.search, + color: Colors.grey, size: 20), suffixIcon: IconButton( - icon: Icon(Icons.clear, color: Colors.grey, size: 18), + icon: Icon(Icons.clear, + color: Colors.grey, size: 18), onPressed: () { setState(() { searchController.clear(); @@ -171,8 +183,10 @@ class _PropertiesForSaleSecondaryState extends State child: Row( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(Icons.search, color: Colors.grey, size: 20), + padding: const EdgeInsets.symmetric( + horizontal: 8.0), + child: Icon(Icons.search, + color: Colors.grey, size: 20), ), Text( S.of(context).search_hint, @@ -200,16 +214,21 @@ class _PropertiesForSaleSecondaryState extends State scrollDirection: Axis.horizontal, child: Row( children: [ - _buildFilterChip("tout", Icons.all_inclusive, selectedOfferType == "tout", context), + _buildFilterChip("tout", Icons.all_inclusive, + selectedOfferType == "tout", context), const SizedBox(width: 8), - _buildFilterChip("vente", Icons.add_shopping_cart, selectedOfferType == "vente", context), + _buildFilterChip("vente", Icons.add_shopping_cart, + selectedOfferType == "vente", context), const SizedBox(width: 8), - _buildFilterChip("achat", Icons.sell, selectedOfferType == "achat", context), + _buildFilterChip("achat", Icons.sell, + selectedOfferType == "achat", context), const SizedBox(width: 8), // Contrôle de visibilité _buildFilterChip( hideNonWhitelisted ? "whitelisted" : "all", - hideNonWhitelisted ? Icons.visibility_off : Icons.visibility, + hideNonWhitelisted + ? Icons.visibility_off + : Icons.visibility, false, context, onTap: () { @@ -217,7 +236,9 @@ class _PropertiesForSaleSecondaryState extends State hideNonWhitelisted = !hideNonWhitelisted; }); }, - customColor: hideNonWhitelisted ? Colors.red.withOpacity(0.2) : Colors.green.withOpacity(0.2), + customColor: hideNonWhitelisted + ? Colors.red.withValues(alpha: 0.2) + : Colors.green.withValues(alpha: 0.2), ), ], ), @@ -228,11 +249,13 @@ class _PropertiesForSaleSecondaryState extends State Container( margin: EdgeInsets.only(left: 8), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.white : Color(0xFF1C1C1E), + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Color(0xFF1C1C1E), borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: Offset(0, 1), ), @@ -243,7 +266,8 @@ class _PropertiesForSaleSecondaryState extends State value: selectedSortOption == "delta" ? "delta" : "date", underline: SizedBox(), isDense: true, - icon: Icon(Icons.keyboard_arrow_down, color: Theme.of(context).primaryColor, size: 18), + icon: Icon(Icons.keyboard_arrow_down, + color: Theme.of(context).primaryColor, size: 18), items: [ DropdownMenuItem( value: "delta", @@ -276,13 +300,19 @@ class _PropertiesForSaleSecondaryState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.arrow_upward, size: 14, color: ascending ? Theme.of(context).primaryColor : Colors.grey), + Icon(Icons.arrow_upward, + size: 14, + color: ascending + ? Theme.of(context).primaryColor + : Colors.grey), SizedBox(width: 3), Text( "Ascendant", style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), - color: ascending ? Theme.of(context).primaryColor : Colors.grey, + color: ascending + ? Theme.of(context).primaryColor + : Colors.grey, ), ), ], @@ -293,13 +323,19 @@ class _PropertiesForSaleSecondaryState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.arrow_downward, size: 14, color: !ascending ? Theme.of(context).primaryColor : Colors.grey), + Icon(Icons.arrow_downward, + size: 14, + color: !ascending + ? Theme.of(context).primaryColor + : Colors.grey), SizedBox(width: 3), Text( "Descendant", style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), - color: !ascending ? Theme.of(context).primaryColor : Colors.grey, + color: !ascending + ? Theme.of(context).primaryColor + : Colors.grey, ), ), ], @@ -346,23 +382,39 @@ class _PropertiesForSaleSecondaryState extends State padding: const EdgeInsets.all(16), itemCount: groupedOffers.keys.length, itemBuilder: (context, index) { - String tokenKey = groupedOffers.keys.elementAt(index); - List> offers = groupedOffers[tokenKey]!; - final imageUrl = (offers.first['imageLink'] != null && offers.first['imageLink'] is List && offers.first['imageLink'].isNotEmpty) ? offers.first['imageLink'][0] : ''; - final shortName = offers.first['shortName'] ?? 'N/A'; + String tokenKey = + groupedOffers.keys.elementAt(index); + List> offers = + groupedOffers[tokenKey]!; + final imageUrl = + (offers.first['imageLink'] != null && + offers.first['imageLink'] is List && + offers.first['imageLink'].isNotEmpty) + ? offers.first['imageLink'][0] + : ''; + final shortName = + offers.first['shortName'] ?? 'N/A'; final country = offers.first['country'] ?? 'USA'; - final String? tokenIdentifier = offers.first['token_to_sell'] ?? offers.first['token_to_buy']; - final bool isWhitelisted = dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == tokenIdentifier?.toLowerCase()); + final String? tokenIdentifier = + offers.first['token_to_sell'] ?? + offers.first['token_to_buy']; + final bool isWhitelisted = + dataManager.whitelistTokens.any((whitelisted) => + whitelisted['token'].toLowerCase() == + tokenIdentifier?.toLowerCase()); // Card style iOS return Container( margin: const EdgeInsets.symmetric(vertical: 3), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.white : Color(0xFF1C1C1E), + color: Theme.of(context).brightness == + Brightness.light + ? Colors.white + : Color(0xFF1C1C1E), borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: Offset(0, 2), ), @@ -387,7 +439,9 @@ class _PropertiesForSaleSecondaryState extends State : CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, - errorWidget: (context, url, error) => const Icon(Icons.error), + errorWidget: (context, url, + error) => + const Icon(Icons.error), ), ), // Badge style iOS pour whitelisted @@ -395,26 +449,45 @@ class _PropertiesForSaleSecondaryState extends State top: 6, right: 6, child: Container( - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: EdgeInsets.symmetric( + horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: isWhitelisted ? Colors.green.withOpacity(0.8) : Colors.red.withOpacity(0.8), - borderRadius: BorderRadius.circular(10), + color: isWhitelisted + ? Colors.green + .withValues(alpha: 0.8) + : Colors.red + .withValues(alpha: 0.8), + borderRadius: + BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - isWhitelisted ? Icons.check_circle : Icons.cancel, + isWhitelisted + ? Icons.check_circle + : Icons.cancel, color: Colors.white, size: 10, ), SizedBox(width: 2), Text( - isWhitelisted ? S.of(context).tokenWhitelisted : S.of(context).tokenNotWhitelisted, + isWhitelisted + ? S + .of(context) + .tokenWhitelisted + : S + .of(context) + .tokenNotWhitelisted, style: TextStyle( color: Colors.white, - fontSize: 9 + Provider.of(context, listen: false).getTextSizeOffset(), - fontWeight: FontWeight.bold, + fontSize: 9 + + Provider.of( + context, + listen: false) + .getTextSizeOffset(), + fontWeight: + FontWeight.bold, ), ), ], @@ -428,7 +501,8 @@ class _PropertiesForSaleSecondaryState extends State Padding( padding: const EdgeInsets.all(12.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ // Nom de la propriété avec icône pays à gauche Row( @@ -437,8 +511,10 @@ class _PropertiesForSaleSecondaryState extends State 'assets/country/${Parameters.getCountryFileName(country)}.png', width: 16, height: 16, - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.flag, size: 16); + errorBuilder: (context, error, + stackTrace) { + return const Icon(Icons.flag, + size: 16); }, ), SizedBox(width: 6), @@ -446,7 +522,9 @@ class _PropertiesForSaleSecondaryState extends State child: Text( shortName, style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), + fontSize: 16 + + appState + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -458,14 +536,36 @@ class _PropertiesForSaleSecondaryState extends State ...offers.map((offer) { bool isTokenWhitelisted = true; if (selectedOfferType == "vente") { - return _buildSaleOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted); - } else if (selectedOfferType == "achat") { - return _buildPurchaseOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted); + return _buildSaleOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted); + } else if (selectedOfferType == + "achat") { + return _buildPurchaseOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted); } else { - if (offer['token_to_buy'] == null) { - return _buildSaleOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted); + if (offer['token_to_buy'] == + null) { + return _buildSaleOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted); } else { - return _buildPurchaseOfferCard(context, appState, currencyUtils, offer, isTokenWhitelisted); + return _buildPurchaseOfferCard( + context, + appState, + currencyUtils, + offer, + isTokenWhitelisted); } } }), @@ -488,10 +588,12 @@ class _PropertiesForSaleSecondaryState extends State } // Widget pour créer un filtre chip style iOS - Widget _buildFilterChip(String type, IconData icon, bool isSelected, BuildContext context, {VoidCallback? onTap, Color? customColor}) { + Widget _buildFilterChip( + String type, IconData icon, bool isSelected, BuildContext context, + {VoidCallback? onTap, Color? customColor}) { final color = customColor ?? (isSelected - ? Theme.of(context).primaryColor.withOpacity(0.2) + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) : Theme.of(context).brightness == Brightness.light ? Colors.white : Color(0xFF1C1C1E)); @@ -509,12 +611,14 @@ class _PropertiesForSaleSecondaryState extends State color: color, borderRadius: BorderRadius.circular(16), border: Border.all( - color: isSelected ? Theme.of(context).primaryColor : Colors.transparent, + color: isSelected + ? Theme.of(context).primaryColor + : Colors.transparent, width: 1, ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: Offset(0, 2), ), @@ -526,14 +630,18 @@ class _PropertiesForSaleSecondaryState extends State Icon( icon, size: 16, - color: isSelected ? Theme.of(context).primaryColor : Colors.grey[600], + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey[600], ), if (type == "whitelisted" || type == "all") SizedBox(width: 4), if (type == "whitelisted") Text( "Whitelisted", style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Colors.grey[600], ), ), @@ -541,7 +649,9 @@ class _PropertiesForSaleSecondaryState extends State Text( "Tous", style: TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 12 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Colors.grey[600], ), ), @@ -561,18 +671,28 @@ class _PropertiesForSaleSecondaryState extends State ) { final dataManager = Provider.of(context, listen: false); // Vérification du statut du token - final String? tokenIdentifier = offer['token_to_sell'] ?? offer['token_to_buy']; - final bool isTokenWhitelisted = dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == tokenIdentifier?.toLowerCase()); - final baseYield = double.tryParse(offer['annualPercentageYield']?.toString() ?? '0') ?? 0; - final initialPrice = double.tryParse(offer['token_price']?.toString() ?? '0') ?? 0; - final offerPrice = double.tryParse(offer['token_value']?.toString() ?? '0') ?? 0; + final String? tokenIdentifier = + offer['token_to_sell'] ?? offer['token_to_buy']; + final bool isTokenWhitelisted = dataManager.whitelistTokens.any( + (whitelisted) => + whitelisted['token'].toLowerCase() == + tokenIdentifier?.toLowerCase()); + final baseYield = + double.tryParse(offer['annualPercentageYield']?.toString() ?? '0') ?? 0; + final initialPrice = + double.tryParse(offer['token_price']?.toString() ?? '0') ?? 0; + final offerPrice = + double.tryParse(offer['token_value']?.toString() ?? '0') ?? 0; - if (baseYield <= 0 || initialPrice <= 0 || offerPrice <= 0) return const SizedBox(); + if (baseYield <= 0 || initialPrice <= 0 || offerPrice <= 0) + return const SizedBox(); final newYield = baseYield * (initialPrice / offerPrice); - final premiumPercentage = ((offerPrice - initialPrice) / initialPrice) * 100; + final premiumPercentage = + ((offerPrice - initialPrice) / initialPrice) * 100; final roiWeeks = (premiumPercentage * 52) / baseYield; - final double deltaValue = ((offer['token_value'] / offer['token_price'] - 1) * 100); + final double deltaValue = + ((offer['token_value'] / offer['token_price'] - 1) * 100); // Définir les couleurs selon le delta avec une palette plus iOS Color deltaColor; @@ -596,7 +716,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6, offset: Offset(0, 1), ), @@ -615,7 +735,9 @@ class _PropertiesForSaleSecondaryState extends State Container( padding: EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -635,7 +757,8 @@ class _PropertiesForSaleSecondaryState extends State ), ), Text( - CustomDateUtils.formatReadableDate(offer['creationDate']), + CustomDateUtils.formatReadableDate( + offer['creationDate']), style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey[500], @@ -655,9 +778,13 @@ class _PropertiesForSaleSecondaryState extends State // Montant du token Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Color(0xFF3A3A3C), + color: Theme.of(context).brightness == + Brightness.light + ? Colors.grey[100] + : Color(0xFF3A3A3C), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -687,9 +814,10 @@ class _PropertiesForSaleSecondaryState extends State // Delta price Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: deltaColor.withOpacity(0.1), + color: deltaColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -705,14 +833,17 @@ class _PropertiesForSaleSecondaryState extends State Row( children: [ Icon( - deltaValue < 0 ? Icons.arrow_downward : Icons.arrow_upward, + deltaValue < 0 + ? Icons.arrow_downward + : Icons.arrow_upward, color: deltaColor, size: 12, ), Text( '${deltaValue.abs().toStringAsFixed(2)}%', style: TextStyle( - fontSize: 14 + appState.getTextSizeOffset(), + fontSize: + 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, color: deltaColor, ), @@ -734,8 +865,8 @@ class _PropertiesForSaleSecondaryState extends State decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.blue.withOpacity(0.1), - deltaColor.withOpacity(0.1), + Colors.blue.withValues(alpha: 0.1), + deltaColor.withValues(alpha: 0.1), ], begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -757,7 +888,8 @@ class _PropertiesForSaleSecondaryState extends State ), ), Text( - currencyUtils.formatCurrency(initialPrice, currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + initialPrice, currencyUtils.currencySymbol), style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -783,7 +915,8 @@ class _PropertiesForSaleSecondaryState extends State ), ), Text( - currencyUtils.formatCurrency(offerPrice, currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + offerPrice, currencyUtils.currencySymbol), style: TextStyle( fontSize: 13 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -804,8 +937,8 @@ class _PropertiesForSaleSecondaryState extends State decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.blue.withOpacity(0.1), - deltaColor.withOpacity(0.1), + Colors.blue.withValues(alpha: 0.1), + deltaColor.withValues(alpha: 0.1), ], begin: Alignment.centerLeft, end: Alignment.centerRight, @@ -876,12 +1009,15 @@ class _PropertiesForSaleSecondaryState extends State child: Container( padding: EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( - color: Color(0xFFFF9500).withOpacity(0.2), // Orange iOS + color: Color(0xFFFF9500) + .withValues(alpha: 0.2), // Orange iOS borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( - S.of(context).roi_label(roiWeeks.toStringAsFixed(1)), + S + .of(context) + .roi_label(roiWeeks.toStringAsFixed(1)), style: TextStyle( fontSize: 12 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -898,12 +1034,15 @@ class _PropertiesForSaleSecondaryState extends State Expanded( child: Material( borderRadius: BorderRadius.circular(10), - color: isTokenWhitelisted ? Color(0xFF007AFF) : Colors.grey, + color: isTokenWhitelisted + ? Color(0xFF007AFF) + : Colors.grey, child: InkWell( borderRadius: BorderRadius.circular(10), onTap: isTokenWhitelisted ? () { - UrlUtils.launchURL('https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); + UrlUtils.launchURL( + 'https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); } : null, child: Container( @@ -914,7 +1053,8 @@ class _PropertiesForSaleSecondaryState extends State style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 12 + appState.getTextSizeOffset(), + fontSize: + 12 + appState.getTextSizeOffset(), ), ), ), @@ -933,7 +1073,10 @@ class _PropertiesForSaleSecondaryState extends State right: 10, child: Builder( builder: (context) { - if (offer['token_to_pay'] == '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || offer['token_to_pay'] == '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { + if (offer['token_to_pay'] == + '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || + offer['token_to_pay'] == + '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { return Container( padding: EdgeInsets.all(3), decoration: BoxDecoration( @@ -941,7 +1084,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: Offset(0, 1), ), @@ -953,7 +1096,10 @@ class _PropertiesForSaleSecondaryState extends State height: 18, ), ); - } else if (offer['token_to_pay'] == '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || offer['token_to_pay'] == '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { + } else if (offer['token_to_pay'] == + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || + offer['token_to_pay'] == + '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { return Container( padding: EdgeInsets.all(3), decoration: BoxDecoration( @@ -961,7 +1107,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: Offset(0, 1), ), @@ -994,15 +1140,21 @@ class _PropertiesForSaleSecondaryState extends State bool isTokenWhitelisted, ) { final dataManager = Provider.of(context, listen: false); - final String? tokenIdentifier = offer['token_to_sell'] ?? offer['token_to_buy']; - final bool isTokenWhitelisted = dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == tokenIdentifier?.toLowerCase()); + final String? tokenIdentifier = + offer['token_to_sell'] ?? offer['token_to_buy']; + final bool isTokenWhitelisted = dataManager.whitelistTokens.any( + (whitelisted) => + whitelisted['token'].toLowerCase() == + tokenIdentifier?.toLowerCase()); - final double deltaValue = ((offer['token_value'] / offer['token_price'] - 1) * 100); + final double deltaValue = + ((offer['token_value'] / offer['token_price'] - 1) * 100); // Définir les couleurs selon le delta avec une palette plus iOS - Color deltaColor = ((offer['token_value'] / offer['token_price'] - 1) * 100) < 0 - ? Color(0xFFFF3B30) // Rouge iOS - : Color(0xFF34C759); // Vert iOS + Color deltaColor = + ((offer['token_value'] / offer['token_price'] - 1) * 100) < 0 + ? Color(0xFFFF3B30) // Rouge iOS + : Color(0xFF34C759); // Vert iOS return Padding( padding: const EdgeInsets.only(bottom: 8.0), @@ -1016,7 +1168,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6, offset: Offset(0, 1), ), @@ -1035,7 +1187,9 @@ class _PropertiesForSaleSecondaryState extends State Container( padding: EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -1055,7 +1209,8 @@ class _PropertiesForSaleSecondaryState extends State ), ), Text( - CustomDateUtils.formatReadableDate(offer['creationDate']), + CustomDateUtils.formatReadableDate( + offer['creationDate']), style: TextStyle( fontSize: 10 + appState.getTextSizeOffset(), color: Colors.grey[500], @@ -1074,9 +1229,13 @@ class _PropertiesForSaleSecondaryState extends State // Montant du token Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Color(0xFF3A3A3C), + color: Theme.of(context).brightness == + Brightness.light + ? Colors.grey[100] + : Color(0xFF3A3A3C), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -1106,9 +1265,13 @@ class _PropertiesForSaleSecondaryState extends State // Valeur du token Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Color(0xFF3A3A3C), + color: Theme.of(context).brightness == + Brightness.light + ? Colors.grey[100] + : Color(0xFF3A3A3C), borderRadius: BorderRadius.circular(10), ), child: Column( @@ -1122,7 +1285,10 @@ class _PropertiesForSaleSecondaryState extends State ), ), Text( - currencyUtils.formatCurrency(currencyUtils.convert(offer['token_value']), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils + .convert(offer['token_value']), + currencyUtils.currencySymbol), style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), fontWeight: FontWeight.bold, @@ -1141,7 +1307,7 @@ class _PropertiesForSaleSecondaryState extends State Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: deltaColor.withOpacity(0.1), + color: deltaColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Row( @@ -1155,7 +1321,9 @@ class _PropertiesForSaleSecondaryState extends State ), ), Icon( - deltaValue < 0 ? Icons.arrow_downward : Icons.arrow_upward, + deltaValue < 0 + ? Icons.arrow_downward + : Icons.arrow_upward, color: deltaColor, size: 14, ), @@ -1176,12 +1344,15 @@ class _PropertiesForSaleSecondaryState extends State // Bouton d'action style iOS Material( borderRadius: BorderRadius.circular(10), - color: isTokenWhitelisted ? Color(0xFF34C759) : Colors.grey, // Vert iOS + color: isTokenWhitelisted + ? Color(0xFF34C759) + : Colors.grey, // Vert iOS child: InkWell( borderRadius: BorderRadius.circular(10), onTap: isTokenWhitelisted ? () { - UrlUtils.launchURL('https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); + UrlUtils.launchURL( + 'https://yambyofferid.netlify.app/?offerId=${offer['id_offer']}'); } : null, child: Container( @@ -1208,7 +1379,10 @@ class _PropertiesForSaleSecondaryState extends State right: 10, child: Builder( builder: (context) { - if (offer['token_to_pay'] == '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || offer['token_to_pay'] == '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { + if (offer['token_to_pay'] == + '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b' || + offer['token_to_pay'] == + '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d') { return Container( padding: EdgeInsets.all(3), decoration: BoxDecoration( @@ -1216,7 +1390,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: Offset(0, 1), ), @@ -1228,7 +1402,10 @@ class _PropertiesForSaleSecondaryState extends State height: 18, ), ); - } else if (offer['token_to_pay'] == '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || offer['token_to_pay'] == '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { + } else if (offer['token_to_pay'] == + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' || + offer['token_to_pay'] == + '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1') { return Container( padding: EdgeInsets.all(3), decoration: BoxDecoration( @@ -1236,7 +1413,7 @@ class _PropertiesForSaleSecondaryState extends State borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: Offset(0, 1), ), diff --git a/lib/pages/propertiesForSale/propertiesForSell_select.dart b/lib/pages/propertiesForSale/propertiesForSell_select.dart index baece83..7b036f6 100644 --- a/lib/pages/propertiesForSale/propertiesForSell_select.dart +++ b/lib/pages/propertiesForSale/propertiesForSell_select.dart @@ -45,7 +45,8 @@ class _PropertiesForSalePageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), child: _buildPageSelector(), ), ], @@ -70,7 +71,9 @@ class _PropertiesForSalePageState extends State { ), ); }, - child: _selectedPage == 'RealT' ? const PropertiesForSaleRealt(key: ValueKey('RealT')) : const PropertiesForSaleSecondary(key: ValueKey('Secondary')), + child: _selectedPage == 'RealT' + ? const PropertiesForSaleRealt(key: ValueKey('RealT')) + : const PropertiesForSaleSecondary(key: ValueKey('Secondary')), ), ), ); @@ -85,7 +88,8 @@ class _PropertiesForSalePageState extends State { ); } - double _calculateTextWidth(BuildContext context, String text, TextStyle style) { + double _calculateTextWidth( + BuildContext context, String text, TextStyle style) { final TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: 1, @@ -106,7 +110,9 @@ class _PropertiesForSalePageState extends State { fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ); - double minWidth = isSelected ? _calculateTextWidth(context, label, textStyle) : 56; // Largeur minimale pour les icônes non sélectionnées + double minWidth = isSelected + ? _calculateTextWidth(context, label, textStyle) + : 56; // Largeur minimale pour les icônes non sélectionnées return isSelected ? Expanded( diff --git a/lib/pages/realt_page.dart b/lib/pages/realt_page.dart index 35599b3..f0dabaa 100644 --- a/lib/pages/realt_page.dart +++ b/lib/pages/realt_page.dart @@ -42,7 +42,8 @@ class RealtPageState extends State { body: SafeArea( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -51,12 +52,16 @@ class RealtPageState extends State { context, 'investment', CupertinoIcons.money_dollar_circle, - currencyUtils.formatCurrency(currencyUtils.convert(dataManager.totalRealtInvestment), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(dataManager.totalRealtInvestment), + currencyUtils.currencySymbol), S.of(context).totalInvestment, [ _buildIOSValueRow( context, - currencyUtils.formatCurrency(currencyUtils.convert(dataManager.netRealtRentYear), currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + currencyUtils.convert(dataManager.netRealtRentYear), + currencyUtils.currencySymbol), 'net rent', ), ], @@ -78,7 +83,9 @@ class RealtPageState extends State { _buildIOSValueRow( context, '${dataManager.rentedRealtUnits}', - S.of(context).rentedUnits(dataManager.rentedUnits.toString(), dataManager.totalUnits.toString()), + S.of(context).rentedUnits( + dataManager.rentedUnits.toString(), + dataManager.totalUnits.toString()), ), _buildIOSValueRow( context, @@ -128,7 +135,7 @@ class RealtPageState extends State { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6, offset: const Offset(0, 3), ), @@ -144,7 +151,7 @@ class RealtPageState extends State { Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: iconColor.withOpacity(0.1), + color: iconColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -157,7 +164,9 @@ class RealtPageState extends State { Text( title, style: TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w600, color: CupertinoColors.label.resolveFrom(context), ), @@ -207,7 +216,9 @@ class RealtPageState extends State { Text( label, style: TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ), diff --git a/lib/pages/realtokens_page.dart b/lib/pages/realtokens_page.dart index 7ccc446..de7d18e 100644 --- a/lib/pages/realtokens_page.dart +++ b/lib/pages/realtokens_page.dart @@ -10,9 +10,7 @@ import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:show_network_image/show_network_image.dart'; import 'package:realtoken_asset_tracker/utils/parameters.dart'; -import 'package:realtoken_asset_tracker/utils/location_utils.dart'; import 'package:realtoken_asset_tracker/components/filter_widgets.dart'; -import 'package:realtoken_asset_tracker/app_state.dart'; class RealTokensPage extends StatefulWidget { const RealTokensPage({super.key}); @@ -49,8 +47,12 @@ class RealTokensPageState extends State { _isAscending = prefs.getBool('isAscending') ?? true; _showNonWhitelisted = prefs.getBool('showNonWhitelisted') ?? true; _filterNotInWallet = prefs.getBool('filterNotInWallet') ?? false; - _selectedRegion = prefs.getString('selectedRegion')?.isEmpty ?? true ? null : prefs.getString('selectedRegion'); - _selectedCountry = prefs.getString('selectedCountry')?.isEmpty ?? true ? null : prefs.getString('selectedCountry'); + _selectedRegion = prefs.getString('selectedRegion')?.isEmpty ?? true + ? null + : prefs.getString('selectedRegion'); + _selectedCountry = prefs.getString('selectedCountry')?.isEmpty ?? true + ? null + : prefs.getString('selectedCountry'); }); } @@ -65,53 +67,82 @@ class RealTokensPageState extends State { } List> _filterAndSortTokens(DataManager dataManager) { - List> filteredTokens = dataManager.allTokens.where((token) { - final matchesSearchQuery = token['fullName'].toLowerCase().contains(_searchQuery.toLowerCase()); - final matchesCity = _selectedCity == null || token['fullName'].contains(_selectedCity!); - final matchesRegion = _selectedRegion == null || (token['regionCode'] != null && token['regionCode'] == _selectedRegion); - final matchesCountry = _selectedCountry == null || _matchesCountryFilter(token, _selectedCountry); + List> filteredTokens = + dataManager.allTokens.where((token) { + final matchesSearchQuery = + token['fullName'].toLowerCase().contains(_searchQuery.toLowerCase()); + final matchesCity = + _selectedCity == null || token['fullName'].contains(_selectedCity!); + final matchesRegion = _selectedRegion == null || + (token['regionCode'] != null && + token['regionCode'] == _selectedRegion); + final matchesCountry = _selectedCountry == null || + _matchesCountryFilter(token, _selectedCountry); - return matchesSearchQuery && matchesCity && matchesRegion && matchesCountry; + return matchesSearchQuery && + matchesCity && + matchesRegion && + matchesCountry; }).toList(); if (_filterNotInWallet) { - filteredTokens = filteredTokens.where((token) => !dataManager.portfolio.any((p) => p['uuid'].toLowerCase() == token['uuid'].toLowerCase())).toList(); + filteredTokens = filteredTokens + .where((token) => !dataManager.portfolio.any( + (p) => p['uuid'].toLowerCase() == token['uuid'].toLowerCase())) + .toList(); } if (!_showNonWhitelisted) { - filteredTokens = filteredTokens.where((token) => dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == token['uuid'].toLowerCase())).toList(); + filteredTokens = filteredTokens + .where((token) => dataManager.whitelistTokens.any((whitelisted) => + whitelisted['token'].toLowerCase() == + token['uuid'].toLowerCase())) + .toList(); } if (_sortOption == S.of(context).sortByName) { - filteredTokens.sort((a, b) => _isAscending ? a['shortName'].compareTo(b['shortName']) : b['shortName'].compareTo(a['shortName'])); + filteredTokens.sort((a, b) => _isAscending + ? a['shortName'].compareTo(b['shortName']) + : b['shortName'].compareTo(a['shortName'])); } else if (_sortOption == S.of(context).sortByValue) { - filteredTokens.sort((a, b) => _isAscending ? a['totalValue'].compareTo(b['totalValue']) : b['totalValue'].compareTo(a['totalValue'])); + filteredTokens.sort((a, b) => _isAscending + ? a['totalValue'].compareTo(b['totalValue']) + : b['totalValue'].compareTo(a['totalValue'])); } else if (_sortOption == S.of(context).sortByAPY) { - filteredTokens.sort((a, b) => _isAscending ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); + filteredTokens.sort((a, b) => _isAscending + ? a['annualPercentageYield'].compareTo(b['annualPercentageYield']) + : b['annualPercentageYield'].compareTo(a['annualPercentageYield'])); } else if (_sortOption == S.of(context).sortByInitialLaunchDate) { - filteredTokens.sort((a, b) => _isAscending ? a['initialLaunchDate'].compareTo(b['initialLaunchDate']) : b['initialLaunchDate'].compareTo(a['initialLaunchDate'])); + filteredTokens.sort((a, b) => _isAscending + ? a['initialLaunchDate'].compareTo(b['initialLaunchDate']) + : b['initialLaunchDate'].compareTo(a['initialLaunchDate'])); } return filteredTokens; } // Méthodes factorisées utilisant FilterWidgets - List _getUniqueCities(List> tokens) => FilterWidgets.getUniqueCities(tokens); - List _getUniqueRegions(List> tokens) => FilterWidgets.getUniqueRegions(tokens); - List _getUniqueCountries(List> tokens) => FilterWidgets.getUniqueCountries(tokens); - + List _getUniqueCities(List> tokens) => + FilterWidgets.getUniqueCities(tokens); + List _getUniqueRegions(List> tokens) => + FilterWidgets.getUniqueRegions(tokens); + List _getUniqueCountries(List> tokens) => + FilterWidgets.getUniqueCountries(tokens); + // Méthode pour vérifier si un token correspond au filtre pays - bool _matchesCountryFilter(Map token, String? selectedCountry) { + bool _matchesCountryFilter( + Map token, String? selectedCountry) { if (selectedCountry == null) return true; - + String tokenCountry = token['country'] ?? "Unknown Country"; - + // Si "Series XX" est sélectionné, filtrer tous les tokens factoring_profitshare avec des séries if (selectedCountry == "Series XX") { - return (token['productType']?.toString().toLowerCase() == 'factoring_profitshare') && - tokenCountry.toLowerCase().startsWith('series '); + return (token['productType']?.toString().toLowerCase() == + 'factoring_profitshare') && + tokenCountry.toLowerCase().startsWith('series '); } - + // Filtre normal return tokenCountry == selectedCountry; } @@ -121,12 +152,13 @@ class RealTokensPageState extends State { required IconData icon, required String label, required VoidCallback onTap, - }) => FilterWidgets.buildFilterButton( - context: context, - icon: icon, - label: label, - onTap: onTap, - ); + }) => + FilterWidgets.buildFilterButton( + context: context, + icon: icon, + label: label, + onTap: onTap, + ); Widget _buildFilterPopupMenu({ required BuildContext context, @@ -140,7 +172,7 @@ class RealTokensPageState extends State { onSelected: onSelected, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context).cardColor.withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -148,13 +180,13 @@ class RealTokensPageState extends State { margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isActive - ? Theme.of(context).primaryColor.withOpacity(0.2) - : Theme.of(context).primaryColor.withOpacity(0.1), + color: isActive + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: isActive - ? Border.all(color: Theme.of(context).primaryColor, width: 2) - : null, + border: isActive + ? Border.all(color: Theme.of(context).primaryColor, width: 2) + : null, ), child: Icon( icon, @@ -211,10 +243,12 @@ class RealTokensPageState extends State { final uniqueCities = _getUniqueCities(dataManager.allTokens); final uniqueRegions = _getUniqueRegions(dataManager.allTokens); final uniqueCountries = _getUniqueCountries(dataManager.allTokens); - final currencyUtils = Provider.of(context, listen: false); + final currencyUtils = + Provider.of(context, listen: false); return NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( floating: true, @@ -225,7 +259,8 @@ class RealTokensPageState extends State { background: Container( color: Theme.of(context).scaffoldBackgroundColor, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 2.0), child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, @@ -236,13 +271,15 @@ class RealTokensPageState extends State { // Conteneur de recherche avec bords arrondis Expanded( child: Container( - margin: const EdgeInsets.only(bottom: 8, right: 8), + margin: const EdgeInsets.only( + bottom: 8, right: 8), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black + .withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2), ), @@ -252,10 +289,12 @@ class RealTokensPageState extends State { children: [ // Icône de recherche Padding( - padding: const EdgeInsets.only(left: 12.0), + padding: + const EdgeInsets.only(left: 12.0), child: Icon( Icons.search, - color: Theme.of(context).primaryColor, + color: + Theme.of(context).primaryColor, size: 18, ), ), @@ -271,13 +310,20 @@ class RealTokensPageState extends State { style: TextStyle(fontSize: 14), decoration: InputDecoration( isDense: true, - hintText: S.of(context).searchHint, + hintText: + S.of(context).searchHint, hintStyle: TextStyle( fontSize: 14, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + contentPadding: + const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12), ), ), ), @@ -304,14 +350,18 @@ class RealTokensPageState extends State { value: S.of(context).allCities, child: Text(S.of(context).allCities), ), - ...uniqueCities.map((city) => PopupMenuItem( - value: city, - child: Text(city), - )), + ...uniqueCities + .map((city) => PopupMenuItem( + value: city, + child: Text(city), + )), ], onSelected: (String value) { setState(() { - _selectedCity = value == S.of(context).allCities ? null : value; + _selectedCity = + value == S.of(context).allCities + ? null + : value; }); }, ), @@ -327,20 +377,30 @@ class RealTokensPageState extends State { value: "all_countries", child: Text("Tous les pays"), ), - ...uniqueCountries.map((country) => PopupMenuItem( + ...uniqueCountries.map((country) => + PopupMenuItem( value: country, child: Row( children: [ - _selectedCountry == country ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + _selectedCountry == country + ? Icon(Icons.check, + size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), - if (country != "Unknown Country") + if (country != + "Unknown Country") Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: + const EdgeInsets.only( + right: 8.0), child: Image.asset( 'assets/country/${Parameters.getCountryFileName(country)}.png', width: 24, height: 16, - errorBuilder: (context, _, __) => const Icon(Icons.flag, size: 20), + errorBuilder: (context, _, + __) => + const Icon(Icons.flag, + size: 20), ), ), Text(country), @@ -350,7 +410,10 @@ class RealTokensPageState extends State { ], onSelected: (String value) { setState(() { - _selectedCountry = value == "all_countries" ? null : value; + _selectedCountry = + value == "all_countries" + ? null + : value; _saveSortPreferences(); }); }, @@ -360,37 +423,53 @@ class RealTokensPageState extends State { _buildFilterPopupMenu( context: context, icon: Icons.map, - label: _selectedRegion != null ? Parameters.getRegionDisplayName(_selectedRegion!) : "Région", + label: _selectedRegion != null + ? Parameters.getRegionDisplayName( + _selectedRegion!) + : "Région", isActive: _selectedRegion != null, items: [ const PopupMenuItem( value: "all_regions", child: Text("Toutes les régions"), ), - ...uniqueRegions.map((region) => PopupMenuItem( + ...uniqueRegions.map((region) => + PopupMenuItem( value: region, child: Row( children: [ - _selectedRegion == region ? Icon(Icons.check, size: 20) : SizedBox(width: 20), + _selectedRegion == region + ? Icon(Icons.check, + size: 20) + : SizedBox(width: 20), SizedBox(width: 8.0), if (region != "Unknown Region") Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: + const EdgeInsets.only( + right: 8.0), child: Image.asset( 'assets/states/${region.toLowerCase()}.png', width: 24, height: 16, - errorBuilder: (context, _, __) => const Icon(Icons.flag, size: 20), + errorBuilder: (context, _, + __) => + const Icon(Icons.flag, + size: 20), ), ), - Text(Parameters.getRegionDisplayName(region)), + Text(Parameters + .getRegionDisplayName( + region)), ], ), )), ], onSelected: (String value) { setState(() { - _selectedRegion = value == "all_regions" ? null : value; + _selectedRegion = value == "all_regions" + ? null + : value; _saveSortPreferences(); }); }, @@ -401,18 +480,21 @@ class RealTokensPageState extends State { context: context, icon: Icons.filter_alt, label: _getFilterLabel(), - isActive: _filterNotInWallet || !_showNonWhitelisted, + isActive: _filterNotInWallet || + !_showNonWhitelisted, items: [ PopupMenuItem( value: "filter_header", enabled: false, child: Row( children: [ - const Icon(Icons.filter_list, size: 20), + const Icon(Icons.filter_list, + size: 20), const SizedBox(width: 8.0), const Text( "Filtres", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold), ), ], ), @@ -422,9 +504,14 @@ class RealTokensPageState extends State { checked: _filterNotInWallet, child: Row( children: [ - const Icon(Icons.account_balance_wallet_outlined, size: 20), + const Icon( + Icons + .account_balance_wallet_outlined, + size: 20), const SizedBox(width: 8.0), - Text(S.of(context).filterNotInWallet), + Text(S + .of(context) + .filterNotInWallet), ], ), ), @@ -433,9 +520,12 @@ class RealTokensPageState extends State { checked: !_showNonWhitelisted, child: Row( children: [ - const Icon(Icons.verified, size: 20), + const Icon(Icons.verified, + size: 20), const SizedBox(width: 8.0), - Text(S.of(context).showOnlyWhitelisted), + Text(S + .of(context) + .showOnlyWhitelisted), ], ), ), @@ -443,9 +533,12 @@ class RealTokensPageState extends State { onSelected: (value) { setState(() { if (value == 'filterNotInWallet') { - _filterNotInWallet = !_filterNotInWallet; - } else if (value == 'showNonWhitelisted') { - _showNonWhitelisted = !_showNonWhitelisted; + _filterNotInWallet = + !_filterNotInWallet; + } else if (value == + 'showNonWhitelisted') { + _showNonWhitelisted = + !_showNonWhitelisted; } _saveSortPreferences(); }); @@ -462,7 +555,8 @@ class RealTokensPageState extends State { tooltip: _getSortLabel(context), onSelected: (String value) { setState(() { - if (value == 'asc' || value == 'desc') { + if (value == 'asc' || + value == 'desc') { _isAscending = (value == 'asc'); } else { _sortOption = value; @@ -472,15 +566,20 @@ class RealTokensPageState extends State { }, offset: const Offset(0, 40), elevation: 8, - color: Theme.of(context).cardColor.withOpacity(0.97), + color: Theme.of(context) + .cardColor + .withValues(alpha: 0.97), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -488,12 +587,14 @@ class RealTokensPageState extends State { Icon( Icons.sort, size: 20, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), Icon( Icons.arrow_drop_down, size: 20, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ], ), @@ -501,23 +602,34 @@ class RealTokensPageState extends State { itemBuilder: (context) => [ CheckedPopupMenuItem( value: S.of(context).sortByName, - checked: _sortOption == S.of(context).sortByName, + checked: _sortOption == + S.of(context).sortByName, child: Text(S.of(context).sortByName), ), CheckedPopupMenuItem( value: S.of(context).sortByValue, - checked: _sortOption == S.of(context).sortByValue, - child: Text(S.of(context).sortByValue), + checked: _sortOption == + S.of(context).sortByValue, + child: + Text(S.of(context).sortByValue), ), CheckedPopupMenuItem( value: S.of(context).sortByAPY, - checked: _sortOption == S.of(context).sortByAPY, + checked: _sortOption == + S.of(context).sortByAPY, child: Text(S.of(context).sortByAPY), ), CheckedPopupMenuItem( - value: S.of(context).sortByInitialLaunchDate, - checked: _sortOption == S.of(context).sortByInitialLaunchDate, - child: Text(S.of(context).sortByInitialLaunchDate), + value: S + .of(context) + .sortByInitialLaunchDate, + checked: _sortOption == + S + .of(context) + .sortByInitialLaunchDate, + child: Text(S + .of(context) + .sortByInitialLaunchDate), ), const PopupMenuDivider(), CheckedPopupMenuItem( @@ -549,7 +661,8 @@ class RealTokensPageState extends State { : GridView.builder( padding: const EdgeInsets.all(12.0), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: MediaQuery.of(context).size.width > 700 ? 2 : 1, + crossAxisCount: + MediaQuery.of(context).size.width > 700 ? 2 : 1, crossAxisSpacing: 16, mainAxisSpacing: 16, mainAxisExtent: 160, @@ -557,18 +670,26 @@ class RealTokensPageState extends State { itemCount: filteredAndSortedTokens.length, itemBuilder: (context, index) { final token = filteredAndSortedTokens[index]; - final bool isInPortfolio = dataManager.portfolio.any((portfolioItem) => portfolioItem['uuid'].toLowerCase() == token['uuid'].toLowerCase()); - final bool isWhitelisted = dataManager.whitelistTokens.any((whitelisted) => whitelisted['token'].toLowerCase() == token['uuid'].toLowerCase()); + final bool isInPortfolio = dataManager.portfolio.any( + (portfolioItem) => + portfolioItem['uuid'].toLowerCase() == + token['uuid'].toLowerCase()); + final bool isWhitelisted = dataManager.whitelistTokens + .any((whitelisted) => + whitelisted['token'].toLowerCase() == + token['uuid'].toLowerCase()); return GestureDetector( onTap: () => showTokenDetails(context, token), child: Container( decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.9), + color: Theme.of(context) + .cardColor + .withValues(alpha: 0.9), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, spreadRadius: 0, offset: const Offset(0, 2), @@ -593,9 +714,15 @@ class RealTokensPageState extends State { : CachedNetworkImage( imageUrl: token['imageLink'][0], fit: BoxFit.cover, - errorWidget: (context, url, error) => Container( - color: Theme.of(context).primaryColor.withOpacity(0.1), - child: const Icon(Icons.image_not_supported, size: 40), + errorWidget: + (context, url, error) => + Container( + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), + child: const Icon( + Icons.image_not_supported, + size: 40), ), ), ), @@ -611,37 +738,47 @@ class RealTokensPageState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withOpacity(0.7), + Colors.black.withValues(alpha: 0.7), Colors.transparent, ], ), ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (isInPortfolio) Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), + color: Colors.white + .withValues(alpha: 0.2), + borderRadius: + BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - Icons.account_balance_wallet, + Icons + .account_balance_wallet, color: Colors.white, size: 12, ), const SizedBox(width: 3), Text( - S.of(context).presentInWallet, + S + .of(context) + .presentInWallet, style: TextStyle( color: Colors.white, fontSize: 10, - fontWeight: FontWeight.w500, + fontWeight: + FontWeight.w500, ), ), ], @@ -653,7 +790,9 @@ class RealTokensPageState extends State { height: 12, decoration: BoxDecoration( shape: BoxShape.circle, - color: isWhitelisted ? Colors.green : Colors.red, + color: isWhitelisted + ? Colors.green + : Colors.red, ), ), ], @@ -668,26 +807,31 @@ class RealTokensPageState extends State { child: Padding( padding: const EdgeInsets.all(12.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ // En-tête avec nom et pays Row( children: [ if (token['country'] != null) Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only( + right: 8.0), child: Image.asset( 'assets/country/${token['country'].toLowerCase()}.png', width: 24, height: 16, - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.flag, size: 18); + errorBuilder: (context, error, + stackTrace) { + return const Icon(Icons.flag, + size: 18); }, ), ), Expanded( child: Text( - token['shortName'] ?? S.of(context).nameUnavailable, + token['shortName'] ?? + S.of(context).nameUnavailable, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 15, @@ -701,12 +845,17 @@ class RealTokensPageState extends State { // Affichage de la région if (token['regionCode'] != null) Padding( - padding: const EdgeInsets.only(top: 2.0), + padding: + const EdgeInsets.only(top: 2.0), child: Text( - Parameters.getRegionDisplayName(token['regionCode']), + Parameters.getRegionDisplayName( + token['regionCode']), style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, @@ -717,26 +866,39 @@ class RealTokensPageState extends State { // Informations de prix Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.5), - borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .cardColor + .withValues(alpha: 0.5), + borderRadius: + BorderRadius.circular(8), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( S.of(context).assetPrice, style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), Text( - currencyUtils.formatCurrency(token['totalInvestment'], currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + token['totalInvestment'], + currencyUtils + .currencySymbol), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, @@ -746,17 +908,25 @@ class RealTokensPageState extends State { ), const SizedBox(height: 2), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( S.of(context).tokenPrice, style: TextStyle( fontSize: 12, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), Text( - currencyUtils.formatCurrency(token['tokenPrice'], currencyUtils.currencySymbol), + currencyUtils.formatCurrency( + token['tokenPrice'], + currencyUtils + .currencySymbol), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, @@ -772,10 +942,14 @@ class RealTokensPageState extends State { // Rendement attendu avec style iOS Container( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -783,7 +957,8 @@ class RealTokensPageState extends State { Icon( Icons.trending_up, size: 14, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), const SizedBox(width: 4), Text( @@ -791,7 +966,8 @@ class RealTokensPageState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .primaryColor, ), ), ], @@ -805,15 +981,25 @@ class RealTokensPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon( - isWhitelisted ? Icons.verified_outlined : Icons.cancel_outlined, - color: isWhitelisted ? Colors.green : Colors.red, + isWhitelisted + ? Icons.verified_outlined + : Icons.cancel_outlined, + color: isWhitelisted + ? Colors.green + : Colors.red, size: 14, ), const SizedBox(width: 4), Text( - isWhitelisted ? S.of(context).tokenWhitelisted : S.of(context).tokenNotWhitelisted, + isWhitelisted + ? S.of(context).tokenWhitelisted + : S + .of(context) + .tokenNotWhitelisted, style: TextStyle( - color: isWhitelisted ? Colors.green : Colors.red, + color: isWhitelisted + ? Colors.green + : Colors.red, fontWeight: FontWeight.w500, fontSize: 11, ), @@ -822,24 +1008,36 @@ class RealTokensPageState extends State { onTap: () { showDialog( context: context, - builder: (context) => AlertDialog( - title: Text(S.of(context).whitelistInfoTitle), - content: Text(S.of(context).whitelistInfoContent), + builder: (context) => + AlertDialog( + title: Text(S + .of(context) + .whitelistInfoTitle), + content: Text(S + .of(context) + .whitelistInfoContent), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: Text(S.of(context).ok), + onPressed: () => + Navigator.pop( + context), + child: Text( + S.of(context).ok), ), ], ), ); }, child: Padding( - padding: const EdgeInsets.only(left: 4.0), + padding: const EdgeInsets.only( + left: 4.0), child: Icon( Icons.info_outline, size: 14, - color: Theme.of(context).textTheme.bodySmall?.color, + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, ), ), ), diff --git a/lib/pages/support_page.dart b/lib/pages/support_page.dart index d090eb2..e7bf0db 100644 --- a/lib/pages/support_page.dart +++ b/lib/pages/support_page.dart @@ -38,14 +38,17 @@ class RealtPageState extends State { 'Support', style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 17 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 17 + + Provider.of(context, listen: false) + .getTextSizeOffset(), ), ), ), body: SafeArea( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -56,7 +59,8 @@ class RealtPageState extends State { 'Contribuez ou signalez un problème sur GitHub :', 'Github isssues link', 'https://github.com/RealToken-Community/realtoken_apps/issues', - Provider.of(context, listen: false).getTextSizeOffset(), + Provider.of(context, listen: false) + .getTextSizeOffset(), iconColor: CupertinoColors.systemGrey, ), const SizedBox(height: 14), @@ -67,7 +71,8 @@ class RealtPageState extends State { 'Rejoignez-nous sur Telegram :', 'Telegram Link here', 'https://t.me/+ae_vCmnjg5JjNWQ0', - Provider.of(context, listen: false).getTextSizeOffset(), + Provider.of(context, listen: false) + .getTextSizeOffset(), iconColor: const Color(0xFF0088CC), ), const SizedBox(height: 14), @@ -78,7 +83,8 @@ class RealtPageState extends State { 'Rejoignez-nous sur Discord :', 'Discord Link here', 'https://discord.com/channels/681940057183092737/681966628527013891', - Provider.of(context, listen: false).getTextSizeOffset(), + Provider.of(context, listen: false) + .getTextSizeOffset(), iconColor: CupertinoColors.systemPurple, ), const SizedBox(height: 14), @@ -107,7 +113,7 @@ class RealtPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 5, offset: const Offset(0, 2), ), @@ -129,7 +135,7 @@ class RealtPageState extends State { Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: iconColor.withOpacity(0.1), + color: iconColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -156,7 +162,8 @@ class RealtPageState extends State { description, style: TextStyle( fontSize: 14 + textSizeOffset, - color: CupertinoColors.secondaryLabel.resolveFrom(context), + color: + CupertinoColors.secondaryLabel.resolveFrom(context), ), ), ), @@ -170,7 +177,8 @@ class RealtPageState extends State { linkText, style: TextStyle( fontSize: 14 + textSizeOffset, - color: CupertinoColors.systemBlue.resolveFrom(context), + color: CupertinoColors.systemBlue + .resolveFrom(context), ), ), ), @@ -198,7 +206,7 @@ class RealtPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 5, offset: const Offset(0, 2), ), @@ -214,7 +222,9 @@ class RealtPageState extends State { Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: CupertinoColors.systemGreen.resolveFrom(context).withOpacity(0.1), + color: CupertinoColors.systemGreen + .resolveFrom(context) + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -246,7 +256,10 @@ class RealtPageState extends State { ), ), const SizedBox(height: 12), - _buildLinkTreeButton(context, Provider.of(context, listen: false).getTextSizeOffset()), + _buildLinkTreeButton( + context, + Provider.of(context, listen: false) + .getTextSizeOffset()), const SizedBox(height: 16), if (kIsWeb || (!kIsWeb && !Platform.isIOS)) Padding( @@ -260,7 +273,8 @@ class RealtPageState extends State { 'PayPal', CupertinoIcons.money_dollar, CupertinoColors.systemBlue, - () => UrlUtils.launchURL('https://paypal.me/byackee?country.x=FR&locale.x=fr_FR'), + () => UrlUtils.launchURL( + 'https://paypal.me/byackee?country.x=FR&locale.x=fr_FR'), appState.getTextSizeOffset(), ), _buildDonationButton( @@ -268,7 +282,8 @@ class RealtPageState extends State { 'Buy Coffee', CupertinoIcons.gift, CupertinoColors.systemBrown, - () => UrlUtils.launchURL('https://buymeacoffee.com/byackee'), + () => UrlUtils.launchURL( + 'https://buymeacoffee.com/byackee'), appState.getTextSizeOffset(), isImage: true, imagePath: 'assets/bmc.png', @@ -278,7 +293,10 @@ class RealtPageState extends State { S.of(context).crypto, CupertinoIcons.bitcoin, CupertinoColors.systemOrange, - () => _showIOSCryptoAddressDialog(context, Provider.of(context, listen: false).getTextSizeOffset()), + () => _showIOSCryptoAddressDialog( + context, + Provider.of(context, listen: false) + .getTextSizeOffset()), appState.getTextSizeOffset(), ), ], @@ -376,7 +394,8 @@ class RealtPageState extends State { ); } - void _showIOSCryptoAddressDialog(BuildContext context, double textSizeOffset) { + void _showIOSCryptoAddressDialog( + BuildContext context, double textSizeOffset) { const cryptoAddress = '0xdc30b07aebaef3f15544a3801c6cb0f35f0118fc'; showCupertinoModalPopup( @@ -459,7 +478,8 @@ class RealtPageState extends State { ); } - void showCupertinoSnackBar({required BuildContext context, required String message}) { + void showCupertinoSnackBar( + {required BuildContext context, required String message}) { final overlayEntry = OverlayEntry( builder: (context) => Positioned( bottom: 50.0, @@ -471,7 +491,7 @@ class RealtPageState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: CupertinoColors.systemGrey.withOpacity(0.9), + color: CupertinoColors.systemGrey.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(10), ), child: Row( diff --git a/lib/pages/tools_page.dart b/lib/pages/tools_page.dart index 255081d..b6353e2 100644 --- a/lib/pages/tools_page.dart +++ b/lib/pages/tools_page.dart @@ -12,7 +12,7 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; class ToolsPage extends StatelessWidget { - const ToolsPage({Key? key}) : super(key: key); + const ToolsPage({super.key}); @override Widget build(BuildContext context) { @@ -24,7 +24,11 @@ class ToolsPage extends StatelessWidget { appBar: AppBar( title: Text( S.of(context).toolsTitle, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset()), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18 + + Provider.of(context, listen: false) + .getTextSizeOffset()), ), centerTitle: true, backgroundColor: Colors.transparent, @@ -37,118 +41,138 @@ class ToolsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - S.of(context).exportRentsTitle, - style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - Text( - S.of(context).exportRentsDescription, - style: theme.textTheme.bodyLarge, - ), - const SizedBox(height: 32), - ElevatedButton.icon( - icon: Image.asset('assets/icons/excel.png', width: 24, height: 24), - label: Text(S.of(context).exportRentsCsv), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: TextStyle(fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600), - backgroundColor: theme.primaryColor, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + Text( + S.of(context).exportRentsTitle, + style: theme.textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), - onPressed: () async { - if (dataManager.rentData.isNotEmpty) { - final csvData = _generateCSV(dataManager.rentData, currencyUtils); - final fileName = 'rents_${_getFormattedToday()}.csv'; - final directory = await getTemporaryDirectory(); - final filePath = '${directory.path}/$fileName'; - final file = File(filePath); - await file.writeAsString(csvData); - await Share.shareXFiles( - [XFile(filePath)], - sharePositionOrigin: Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), - width: 100, - height: 100, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).noRentDataToShare, style: TextStyle(color: theme.textTheme.bodyLarge?.color)), - behavior: SnackBarBehavior.floating, - backgroundColor: theme.cardColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - margin: const EdgeInsets.all(10), - ), - ); - } - }, - ), - const SizedBox(height: 24), - Divider(height: 32), - Text( - S.of(context).exportAllTransactionsTitle, - style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - Text( - S.of(context).exportAllTransactionsDescription, - style: theme.textTheme.bodyLarge, - ), - const SizedBox(height: 32), - ElevatedButton.icon( - icon: Image.asset('assets/icons/excel.png', width: 24, height: 24), - label: Text(S.of(context).exportAllTransactionsCsv), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: TextStyle(fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), fontWeight: FontWeight.w600), - backgroundColor: theme.primaryColor, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + const SizedBox(height: 16), + Text( + S.of(context).exportRentsDescription, + style: theme.textTheme.bodyLarge, ), - onPressed: () async { - final allEvents = _getAllEvents(dataManager, currencyUtils); - if (allEvents.isNotEmpty) { - final csvData = _generateAllEventsCSV(allEvents); - final fileName = 'transactions_${_getFormattedToday()}.csv'; - final directory = await getTemporaryDirectory(); - final filePath = '${directory.path}/$fileName'; - final file = File(filePath); - await file.writeAsString(csvData); - await Share.shareXFiles( - [XFile(filePath)], - sharePositionOrigin: Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), - width: 100, - height: 100, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).noTransactionOrRentToExport, style: TextStyle(color: theme.textTheme.bodyLarge?.color)), - behavior: SnackBarBehavior.floating, - backgroundColor: theme.cardColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + const SizedBox(height: 32), + ElevatedButton.icon( + icon: Image.asset('assets/icons/excel.png', + width: 24, height: 24), + label: Text(S.of(context).exportRentsCsv), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: TextStyle( + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600), + backgroundColor: theme.primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () async { + if (dataManager.rentData.isNotEmpty) { + final csvData = + _generateCSV(dataManager.rentData, currencyUtils); + final fileName = 'rents_${_getFormattedToday()}.csv'; + final directory = await getTemporaryDirectory(); + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsString(csvData); + await Share.shareXFiles( + [XFile(filePath)], + sharePositionOrigin: Rect.fromCenter( + center: MediaQuery.of(context).size.center(Offset.zero), + width: 100, + height: 100, ), - margin: const EdgeInsets.all(10), - ), - ); - } - }, - ), - const SizedBox(height: 24), - if (dataManager.rentData.isEmpty) + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).noRentDataToShare, + style: TextStyle( + color: theme.textTheme.bodyLarge?.color)), + behavior: SnackBarBehavior.floating, + backgroundColor: theme.cardColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.all(10), + ), + ); + } + }, + ), + const SizedBox(height: 24), + Divider(height: 32), Text( - S.of(context).noRentDataAvailable, - style: theme.textTheme.bodyLarge?.copyWith(color: theme.colorScheme.error), - textAlign: TextAlign.center, + S.of(context).exportAllTransactionsTitle, + style: theme.textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + S.of(context).exportAllTransactionsDescription, + style: theme.textTheme.bodyLarge, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + icon: Image.asset('assets/icons/excel.png', + width: 24, height: 24), + label: Text(S.of(context).exportAllTransactionsCsv), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: TextStyle( + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), + fontWeight: FontWeight.w600), + backgroundColor: theme.primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () async { + final allEvents = _getAllEvents(dataManager, currencyUtils); + if (allEvents.isNotEmpty) { + final csvData = _generateAllEventsCSV(allEvents); + final fileName = 'transactions_${_getFormattedToday()}.csv'; + final directory = await getTemporaryDirectory(); + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsString(csvData); + await Share.shareXFiles( + [XFile(filePath)], + sharePositionOrigin: Rect.fromCenter( + center: MediaQuery.of(context).size.center(Offset.zero), + width: 100, + height: 100, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).noTransactionOrRentToExport, + style: TextStyle( + color: theme.textTheme.bodyLarge?.color)), + behavior: SnackBarBehavior.floating, + backgroundColor: theme.cardColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.all(10), + ), + ); + } + }, ), + const SizedBox(height: 24), + if (dataManager.rentData.isEmpty) + Text( + S.of(context).noRentDataAvailable, + style: theme.textTheme.bodyLarge + ?.copyWith(color: theme.colorScheme.error), + textAlign: TextAlign.center, + ), ], ), ), @@ -176,7 +200,8 @@ class ToolsPage extends StatelessWidget { return csvBuffer.toString(); } - List> _getAllEvents(DataManager dataManager, CurrencyProvider currencyUtils) { + List> _getAllEvents( + DataManager dataManager, CurrencyProvider currencyUtils) { final List> events = []; // Transactions du portfolio for (var token in dataManager.portfolio) { @@ -190,7 +215,8 @@ class ToolsPage extends StatelessWidget { 'date': date, 'type': 'transaction', 'amount': transaction['amount'], - 'price': currencyUtils.convert(transaction['price'] ?? token['tokenPrice']), + 'price': currencyUtils + .convert(transaction['price'] ?? token['tokenPrice']), 'currency': currencyUtils.currencySymbol, 'fullName': token['fullName'], 'uuid': token['uuid'] ?? '', @@ -203,7 +229,9 @@ class ToolsPage extends StatelessWidget { // Loyers for (var rent in dataManager.rentData) { if (rent['date'] != null) { - DateTime date = rent['date'] is String ? DateTime.parse(rent['date']) : rent['date']; + DateTime date = rent['date'] is String + ? DateTime.parse(rent['date']) + : rent['date']; events.add({ 'date': date, 'type': 'rent', @@ -223,18 +251,24 @@ class ToolsPage extends StatelessWidget { String _generateAllEventsCSV(List> events) { final StringBuffer csvBuffer = StringBuffer(); - csvBuffer.writeln('Date,Type,Montant,Prix,Devise,Nom du token,UUID,Type de transaction'); + csvBuffer.writeln( + 'Date,Type,Montant,Prix,Devise,Nom du token,UUID,Type de transaction'); for (var event in events) { final String date = DateFormat('yyyy-MM-dd').format(event['date']); final String type = event['type'] == 'rent' ? 'Loyer' : 'Transaction'; - final String amount = event['amount'] != null ? event['amount'].toString() : ''; - final String price = event['price'] != null && event['price'].toString().isNotEmpty ? event['price'].toString() : ''; + final String amount = + event['amount'] != null ? event['amount'].toString() : ''; + final String price = + event['price'] != null && event['price'].toString().isNotEmpty + ? event['price'].toString() + : ''; final String currency = event['currency'] ?? ''; final String fullName = event['fullName'] ?? ''; final String uuid = event['uuid'] ?? ''; final String transactionType = event['transactionType'] ?? ''; - csvBuffer.writeln('$date,$type,$amount,$price,$currency,$fullName,$uuid,$transactionType'); + csvBuffer.writeln( + '$date,$type,$amount,$price,$currency,$fullName,$uuid,$transactionType'); } return csvBuffer.toString(); } -} \ No newline at end of file +} diff --git a/lib/pages/updates_page.dart b/lib/pages/updates_page.dart index 322838f..81cc6dc 100644 --- a/lib/pages/updates_page.dart +++ b/lib/pages/updates_page.dart @@ -28,7 +28,7 @@ class _UpdatesPageState extends State { @override void initState() { super.initState(); - + WidgetsBinding.instance.addPostFrameCallback((_) { Provider.of(context, listen: false).fetchAndStoreAllTokens(); }); @@ -46,9 +46,9 @@ class _UpdatesPageState extends State { type: MaterialType.transparency, child: DefaultTextStyle( style: TextStyle( - color: MediaQuery.of(context).platformBrightness == Brightness.dark - ? Colors.white - : Colors.black, + color: MediaQuery.of(context).platformBrightness == Brightness.dark + ? Colors.white + : Colors.black, fontSize: 14, ), child: CupertinoTheme( @@ -57,320 +57,368 @@ class _UpdatesPageState extends State { ), child: Builder( builder: (context) { - final dataManager = Provider.of(context); - final appState = Provider.of(context); - final isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark; - - // Obtenir les changements récents - List> recentChanges = dataManager.getRecentTokenChanges( - days: showAllChanges ? null : 365, // null = pas de limite, sinon 1 an par défaut - includeAllChanges: includeAllFieldChanges - ); - - // Filtrer pour les tokens de l'utilisateur uniquement si nécessaire - if (showUserTokensOnly) { - Set userTokenUuids = dataManager.portfolio.map((token) => - (token['uuid']?.toLowerCase() ?? '') as String - ).toSet(); - - recentChanges = recentChanges.where((change) => - userTokenUuids.contains(change['token_uuid']?.toLowerCase() ?? '') - ).toList(); - } - - if (recentChanges.isEmpty) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: CupertinoColors.systemBackground.resolveFrom(context).withOpacity(0.8), - border: null, - middle: Text( - S.of(context).recentUpdatesTitle, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 17 + appState.getTextSizeOffset(), - ), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - CupertinoIcons.clock, - size: 56, - color: CupertinoColors.systemGrey, - ), - SizedBox(height: 20), - Text( - "Aucune modification récente", - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 17 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 8), - Text( - showAllChanges - ? "Aucun changement trouvé dans l'historique complet" - : "Aucun changement trouvé dans l'année écoulée", - style: TextStyle( - color: CupertinoColors.systemGrey2, - fontSize: 14 + appState.getTextSizeOffset(), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - // Grouper les changements par date - Map>> changesByDate = {}; - for (var change in recentChanges) { - String date = change['date'] ?? ''; - if (date.isNotEmpty) { - if (!changesByDate.containsKey(date)) { - changesByDate[date] = []; - } - changesByDate[date]!.add(change); - } - } - - // Trier les dates (plus récente en premier) - List sortedDates = changesByDate.keys.toList() - ..sort((a, b) => b.compareTo(a)); - - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: CupertinoColors.systemBackground.resolveFrom(context).withOpacity(0.8), - border: null, - middle: Text( - S.of(context).recentUpdatesTitle, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 17 + appState.getTextSizeOffset(), - ), - ), - ), - child: SafeArea( - child: Column( - children: [ - // En-tête avec filtres - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - decoration: BoxDecoration( - color: CupertinoColors.systemBackground.resolveFrom(context).withOpacity(0.7), - border: Border( - bottom: BorderSide( - color: CupertinoColors.systemGrey5.resolveFrom(context).withOpacity(0.5), - width: 0.5, + final dataManager = Provider.of(context); + final appState = Provider.of(context); + final isDarkMode = + MediaQuery.of(context).platformBrightness == Brightness.dark; + + // Obtenir les changements récents + List> recentChanges = + dataManager.getRecentTokenChanges( + days: showAllChanges + ? null + : 365, // null = pas de limite, sinon 1 an par défaut + includeAllChanges: includeAllFieldChanges); + + // Filtrer pour les tokens de l'utilisateur uniquement si nécessaire + if (showUserTokensOnly) { + Set userTokenUuids = dataManager.portfolio + .map((token) => + (token['uuid']?.toLowerCase() ?? '') as String) + .toSet(); + + recentChanges = recentChanges + .where((change) => userTokenUuids + .contains(change['token_uuid']?.toLowerCase() ?? '')) + .toList(); + } + + if (recentChanges.isEmpty) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + backgroundColor: CupertinoColors.systemBackground + .resolveFrom(context) + .withValues(alpha: 0.8), + border: null, + middle: Text( + S.of(context).recentUpdatesTitle, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 17 + appState.getTextSizeOffset(), ), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // Premier switch: Portfolio uniquement - Expanded( - child: Column( - children: [ - Text( - "Portfolio", - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: CupertinoColors.label.resolveFrom(context), - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Transform.scale( - scale: 0.8, - child: CupertinoSwitch( - value: showUserTokensOnly, - onChanged: (value) { - setState(() { - showUserTokensOnly = value; - }); - }, - activeColor: Theme.of(context).primaryColor, - trackColor: isDarkMode - ? CupertinoColors.systemGrey4.darkColor - : CupertinoColors.systemGrey5.color, - ), - ), - ], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.clock, + size: 56, + color: CupertinoColors.systemGrey, ), - ), - - // Deuxième switch: Inclure tous les champs - Expanded( - child: Column( - children: [ - Text( - "Tous les\nchangements", - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: CupertinoColors.label.resolveFrom(context), - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Transform.scale( - scale: 0.8, - child: CupertinoSwitch( - value: includeAllFieldChanges, - onChanged: (value) { - setState(() { - includeAllFieldChanges = value; - }); - }, - activeColor: Theme.of(context).primaryColor, - trackColor: isDarkMode - ? CupertinoColors.systemGrey4.darkColor - : CupertinoColors.systemGrey5.color, - ), - ), - ], + SizedBox(height: 20), + Text( + "Aucune modification récente", + style: TextStyle( + color: CupertinoColors.systemGrey, + fontSize: 17 + appState.getTextSizeOffset(), + fontWeight: FontWeight.w400, + ), ), - ), - - // Troisième switch: Tous les changements - Expanded( - child: Column( - children: [ - Text( - "Historique\nComplet", - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: CupertinoColors.label.resolveFrom(context), - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Transform.scale( - scale: 0.8, - child: CupertinoSwitch( - value: showAllChanges, - onChanged: (value) { - setState(() { - showAllChanges = value; - }); - }, - activeColor: Theme.of(context).primaryColor, - trackColor: isDarkMode - ? CupertinoColors.systemGrey4.darkColor - : CupertinoColors.systemGrey5.color, - ), - ), - ], + SizedBox(height: 8), + Text( + showAllChanges + ? "Aucun changement trouvé dans l'historique complet" + : "Aucun changement trouvé dans l'année écoulée", + style: TextStyle( + color: CupertinoColors.systemGrey2, + fontSize: 14 + appState.getTextSizeOffset(), + ), + textAlign: TextAlign.center, ), - ), - ], + ], + ), + ), + ); + } + + // Grouper les changements par date + Map>> changesByDate = {}; + for (var change in recentChanges) { + String date = change['date'] ?? ''; + if (date.isNotEmpty) { + if (!changesByDate.containsKey(date)) { + changesByDate[date] = []; + } + changesByDate[date]!.add(change); + } + } + + // Trier les dates (plus récente en premier) + List sortedDates = changesByDate.keys.toList() + ..sort((a, b) => b.compareTo(a)); + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + backgroundColor: CupertinoColors.systemBackground + .resolveFrom(context) + .withValues(alpha: 0.8), + border: null, + middle: Text( + S.of(context).recentUpdatesTitle, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 17 + appState.getTextSizeOffset(), + ), ), ), - ), - ), - - // Liste des changements - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: sortedDates.length, - itemBuilder: (context, index) { - String date = sortedDates[index]; - List> changesForDate = changesByDate[date]!; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: SafeArea( + child: Column( children: [ - // En-tête de date aligné avec la timeline - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - // Espace pour aligner avec la timeline (32px de largeur) - Container( - width: 32, - child: Center( - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Theme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - spreadRadius: 2, - ), - ], - ), - child: Icon( - CupertinoIcons.calendar, - size: 24, - color: CupertinoColors.white, - ), + // En-tête avec filtres + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: CupertinoColors.systemBackground + .resolveFrom(context) + .withValues(alpha: 0.7), + border: Border( + bottom: BorderSide( + color: CupertinoColors.systemGrey5 + .resolveFrom(context) + .withValues(alpha: 0.5), + width: 0.5, ), ), ), - SizedBox(width: 12), - - // Contenu de la date - Expanded( - child: Row( - children: [ - Text( - _formatDate(date), - style: TextStyle( - fontSize: 16 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w600, - color: CupertinoColors.label.resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Premier switch: Portfolio uniquement + Expanded( + child: Column( + children: [ + Text( + "Portfolio", + style: TextStyle( + fontSize: + 12 + appState.getTextSizeOffset(), + fontWeight: FontWeight.w500, + color: CupertinoColors.label + .resolveFrom(context), + ), + textAlign: TextAlign.center, ), - ), - Spacer(), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + SizedBox(height: 4), + Transform.scale( + scale: 0.8, + child: CupertinoSwitch( + value: showUserTokensOnly, + onChanged: (value) { + setState(() { + showUserTokensOnly = value; + }); + }, + activeTrackColor: + Theme.of(context).primaryColor, + inactiveTrackColor: isDarkMode + ? CupertinoColors + .systemGrey4.darkColor + : CupertinoColors + .systemGrey5.color, + ), + ), + ], ), - child: Text( - "${changesForDate.length} modification${changesForDate.length > 1 ? 's' : ''}", - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - fontWeight: FontWeight.w500, - color: Theme.of(context).primaryColor, - ), + ), + + // Deuxième switch: Inclure tous les champs + Expanded( + child: Column( + children: [ + Text( + "Tous les\nchangements", + style: TextStyle( + fontSize: + 12 + appState.getTextSizeOffset(), + fontWeight: FontWeight.w500, + color: CupertinoColors.label + .resolveFrom(context), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Transform.scale( + scale: 0.8, + child: CupertinoSwitch( + value: includeAllFieldChanges, + onChanged: (value) { + setState(() { + includeAllFieldChanges = value; + }); + }, + activeTrackColor: + Theme.of(context).primaryColor, + inactiveTrackColor: isDarkMode + ? CupertinoColors + .systemGrey4.darkColor + : CupertinoColors + .systemGrey5.color, + ), + ), + ], ), ), - ], + + // Troisième switch: Tous les changements + Expanded( + child: Column( + children: [ + Text( + "Historique\nComplet", + style: TextStyle( + fontSize: + 12 + appState.getTextSizeOffset(), + fontWeight: FontWeight.w500, + color: CupertinoColors.label + .resolveFrom(context), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Transform.scale( + scale: 0.8, + child: CupertinoSwitch( + value: showAllChanges, + onChanged: (value) { + setState(() { + showAllChanges = value; + }); + }, + activeTrackColor: + Theme.of(context).primaryColor, + inactiveTrackColor: isDarkMode + ? CupertinoColors + .systemGrey4.darkColor + : CupertinoColors + .systemGrey5.color, + ), + ), + ], + ), ), - ), - - ], + ], + ), + ), ), ), - // Grouper les changements par token pour cette date avec timeline - ..._buildChangesForDateWithTimeline(context, changesForDate, appState, index, sortedDates.length), + // Liste des changements + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: sortedDates.length, + itemBuilder: (context, index) { + String date = sortedDates[index]; + List> changesForDate = + changesByDate[date]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête de date aligné avec la timeline + Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + // Espace pour aligner avec la timeline (32px de largeur) + SizedBox( + width: 32, + child: Center( + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .primaryColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: Icon( + CupertinoIcons.calendar, + size: 24, + color: CupertinoColors.white, + ), + ), + ), + ), + SizedBox(width: 12), + + // Contenu de la date + Expanded( + child: Row( + children: [ + Text( + _formatDate(date), + style: TextStyle( + fontSize: 16 + + appState + .getTextSizeOffset(), + fontWeight: FontWeight.w600, + color: CupertinoColors.label + .resolveFrom(context), + ), + ), + Spacer(), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(12), + ), + child: Text( + "${changesForDate.length} modification${changesForDate.length > 1 ? 's' : ''}", + style: TextStyle( + fontSize: 12 + + appState + .getTextSizeOffset(), + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryColor, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + // Grouper les changements par token pour cette date avec timeline + ..._buildChangesForDateWithTimeline( + context, + changesForDate, + appState, + index, + sortedDates.length), + ], + ); + }, + ), + ), ], - ); - }, - ), - ), - ], - ), - ), - ); + ), + ), + ); }, ), ), @@ -378,7 +426,12 @@ class _UpdatesPageState extends State { ); } - List _buildChangesForDateWithTimeline(BuildContext context, List> changes, AppState appState, int dateIndex, int totalDates) { + List _buildChangesForDateWithTimeline( + BuildContext context, + List> changes, + AppState appState, + int dateIndex, + int totalDates) { // Grouper par token Map>> changesByToken = {}; for (var change in changes) { @@ -391,11 +444,11 @@ class _UpdatesPageState extends State { List widgets = []; List tokenKeys = changesByToken.keys.toList(); - + for (int tokenIndex = 0; tokenIndex < tokenKeys.length; tokenIndex++) { String tokenUuid = tokenKeys[tokenIndex]; List> tokenChanges = changesByToken[tokenUuid]!; - + if (tokenChanges.isNotEmpty) { var firstChange = tokenChanges.first; String shortName = firstChange['shortName'] ?? 'Token inconnu'; @@ -410,10 +463,12 @@ class _UpdatesPageState extends State { // Récupérer les informations du token complet pour obtenir le pays final dataManager = Provider.of(context, listen: false); - Map? fullTokenInfo = dataManager.allTokens.cast?>().firstWhere( - (token) => token?['uuid']?.toLowerCase() == tokenUuid.toLowerCase(), - orElse: () => null, - ); + Map? fullTokenInfo = + dataManager.allTokens.cast?>().firstWhere( + (token) => + token?['uuid']?.toLowerCase() == tokenUuid.toLowerCase(), + orElse: () => null, + ); String? country = fullTokenInfo?['country']; widgets.add( @@ -425,124 +480,137 @@ class _UpdatesPageState extends State { children: [ // Timeline sur la gauche avec hauteur adaptative _buildTimelineIndicator( - context, - dateIndex, - tokenIndex, - totalDates, - tokenKeys.length, - _getChangeColor(tokenChanges) - ), + context, + dateIndex, + tokenIndex, + totalDates, + tokenKeys.length, + _getChangeColor(tokenChanges)), SizedBox(width: 12), - + // Carte du token Expanded( child: Container( - decoration: BoxDecoration( - color: CupertinoColors.systemBackground.resolveFrom(context), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: CupertinoColors.systemGrey6.resolveFrom(context).withOpacity(0.6), - blurRadius: 12, - spreadRadius: 0, - offset: Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // En-tête du token avec image - if (imageUrl.isNotEmpty) - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - child: AspectRatio( - aspectRatio: 16 / 9, - child: kIsWeb - ? ShowNetworkImage( - imageSrc: imageUrl, - mobileBoxFit: BoxFit.cover, - ) - : CachedNetworkImage( - imageUrl: imageUrl, - fit: BoxFit.cover, - placeholder: (context, url) => Center( - child: CupertinoActivityIndicator( - radius: 14, - color: CupertinoColors.activeBlue, + decoration: BoxDecoration( + color: CupertinoColors.systemBackground + .resolveFrom(context), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: CupertinoColors.systemGrey6 + .resolveFrom(context) + .withValues(alpha: 0.6), + blurRadius: 12, + spreadRadius: 0, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête du token avec image + if (imageUrl.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: kIsWeb + ? ShowNetworkImage( + imageSrc: imageUrl, + mobileBoxFit: BoxFit.cover, + ) + : CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + placeholder: (context, url) => Center( + child: CupertinoActivityIndicator( + radius: 14, + color: CupertinoColors.activeBlue, + ), ), - ), - errorWidget: (context, url, error) => Container( - color: CupertinoColors.systemGrey6, - child: Icon( - CupertinoIcons.exclamationmark_triangle, - color: CupertinoColors.systemGrey, - size: 36, + errorWidget: (context, url, error) => + Container( + color: CupertinoColors.systemGrey6, + child: Icon( + CupertinoIcons + .exclamationmark_triangle, + color: CupertinoColors.systemGrey, + size: 36, + ), ), ), - ), + ), ), - ), - // Contenu de la carte - Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Nom du token avec indicateur - Row( - children: [ - if (country != null) - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.asset( - 'assets/country/${country.toLowerCase()}.png', - width: 20 + appState.getTextSizeOffset(), - height: 20 + appState.getTextSizeOffset(), - errorBuilder: (context, error, stackTrace) { - return Icon( - CupertinoIcons.flag, - size: 20 + appState.getTextSizeOffset(), - color: CupertinoColors.systemGrey, - ); - }, - ), - ), - if (country == null) - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: _getChangeColor(tokenChanges), + // Contenu de la carte + Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nom du token avec indicateur + Row( + children: [ + if (country != null) + ClipRRect( borderRadius: BorderRadius.circular(4), + child: Image.asset( + 'assets/country/${country.toLowerCase()}.png', + width: + 20 + appState.getTextSizeOffset(), + height: + 20 + appState.getTextSizeOffset(), + errorBuilder: + (context, error, stackTrace) { + return Icon( + CupertinoIcons.flag, + size: 20 + + appState.getTextSizeOffset(), + color: CupertinoColors.systemGrey, + ); + }, + ), ), - ), - SizedBox(width: 8), - Expanded( - child: Text( - shortName, - style: TextStyle( - fontSize: 18 + appState.getTextSizeOffset(), - fontWeight: FontWeight.bold, - color: CupertinoColors.label.resolveFrom(context), + if (country == null) + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: _getChangeColor(tokenChanges), + borderRadius: + BorderRadius.circular(4), + ), + ), + SizedBox(width: 8), + Expanded( + child: Text( + shortName, + style: TextStyle( + fontSize: + 18 + appState.getTextSizeOffset(), + fontWeight: FontWeight.bold, + color: CupertinoColors.label + .resolveFrom(context), + ), ), ), - ), - ], - ), - SizedBox(height: 12), + ], + ), + SizedBox(height: 12), - // Liste des changements pour ce token - ...tokenChanges.map((change) => _buildChangeItem(context, change, appState)).toList(), - ], + // Liste des changements pour ce token + ...tokenChanges.map((change) => + _buildChangeItem( + context, change, appState)), + ], + ), ), - ), - ], - ), + ], + ), ), ), ], @@ -556,7 +624,8 @@ class _UpdatesPageState extends State { return widgets; } - Widget _buildTimelineIndicator(BuildContext context, int dateIndex, int tokenIndex, int totalDates, int tokensInDate, Color changeColor) { + Widget _buildTimelineIndicator(BuildContext context, int dateIndex, + int tokenIndex, int totalDates, int tokensInDate, Color changeColor) { bool isFirstDate = dateIndex == 0; bool isLastDate = dateIndex == totalDates - 1; bool isFirstToken = tokenIndex == 0; @@ -564,7 +633,7 @@ class _UpdatesPageState extends State { bool isVeryFirstItem = isFirstDate && isFirstToken; bool isVeryLastItem = isLastDate && isLastToken; - return Container( + return SizedBox( width: 32, // Pas de hauteur fixe - s'adapte à la hauteur de la carte child: Stack( @@ -577,7 +646,8 @@ class _UpdatesPageState extends State { if (!isVeryFirstItem) Expanded( child: Transform.translate( - offset: Offset(0, -8), // Remonte dans l'espace entre les cartes + offset: + Offset(0, -8), // Remonte dans l'espace entre les cartes child: Container( width: 2, height: double.infinity, @@ -586,8 +656,12 @@ class _UpdatesPageState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - CupertinoColors.systemGrey4.resolveFrom(context).withOpacity(0.6), - CupertinoColors.systemGrey3.resolveFrom(context).withOpacity(0.8), + CupertinoColors.systemGrey4 + .resolveFrom(context) + .withValues(alpha: 0.6), + CupertinoColors.systemGrey3 + .resolveFrom(context) + .withValues(alpha: 0.8), ], ), ), @@ -598,7 +672,8 @@ class _UpdatesPageState extends State { if (!isVeryLastItem) Expanded( child: Transform.translate( - offset: Offset(0, 8), // Descend dans l'espace entre les cartes + offset: + Offset(0, 8), // Descend dans l'espace entre les cartes child: Container( width: 2, height: double.infinity, @@ -607,8 +682,12 @@ class _UpdatesPageState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - CupertinoColors.systemGrey3.resolveFrom(context).withOpacity(0.8), - CupertinoColors.systemGrey4.resolveFrom(context).withOpacity(0.6), + CupertinoColors.systemGrey3 + .resolveFrom(context) + .withValues(alpha: 0.8), + CupertinoColors.systemGrey4 + .resolveFrom(context) + .withValues(alpha: 0.6), ], ), ), @@ -617,7 +696,7 @@ class _UpdatesPageState extends State { ), ], ), - + // Point principal de la timeline par-dessus les lignes Container( width: 16, @@ -631,7 +710,7 @@ class _UpdatesPageState extends State { ), boxShadow: [ BoxShadow( - color: changeColor.withOpacity(0.3), + color: changeColor.withValues(alpha: 0.3), blurRadius: 8, spreadRadius: 2, ), @@ -653,7 +732,8 @@ class _UpdatesPageState extends State { ); } - Widget _buildChangeItem(BuildContext context, Map change, AppState appState) { + Widget _buildChangeItem( + BuildContext context, Map change, AppState appState) { String fieldLabel = change['fieldLabel'] ?? 'Champ inconnu'; String changeType = change['changeType'] ?? 'change'; var previousValue = change['previousValue']; @@ -666,10 +746,10 @@ class _UpdatesPageState extends State { margin: EdgeInsets.only(bottom: 8), padding: EdgeInsets.all(12), decoration: BoxDecoration( - color: changeColor.withOpacity(0.05), + color: changeColor.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( - color: changeColor.withOpacity(0.2), + color: changeColor.withValues(alpha: 0.2), width: 1, ), ), @@ -704,21 +784,21 @@ class _UpdatesPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Ancien:", - style: TextStyle( - fontSize: 12 + appState.getTextSizeOffset(), - color: CupertinoColors.systemGrey.resolveFrom(context), + Text( + "Ancien:", + style: TextStyle( + fontSize: 12 + appState.getTextSizeOffset(), + color: CupertinoColors.systemGrey.resolveFrom(context), + ), ), - ), - Text( - _formatValue(previousValue, change['field']), - style: TextStyle( - fontSize: 13 + appState.getTextSizeOffset(), - decoration: TextDecoration.lineThrough, - color: CupertinoColors.systemGrey.resolveFrom(context), + Text( + _formatValue(previousValue, change['field']), + style: TextStyle( + fontSize: 13 + appState.getTextSizeOffset(), + decoration: TextDecoration.lineThrough, + color: CupertinoColors.systemGrey.resolveFrom(context), + ), ), - ), ], ), ), @@ -767,9 +847,9 @@ class _UpdatesPageState extends State { DateTime now = DateTime.now(); DateTime today = DateTime(now.year, now.month, now.day); DateTime targetDate = DateTime(date.year, date.month, date.day); - + int difference = today.difference(targetDate).inDays; - + if (difference == 0) { return "Aujourd'hui"; } else if (difference == 1) { @@ -786,9 +866,12 @@ class _UpdatesPageState extends State { String _formatValue(dynamic value, String? field) { if (value == null) return 'N/A'; - + if (value is num) { - if (field?.contains('price') == true || field?.contains('investment') == true || field?.contains('rent') == true || field?.contains('reserve') == true) { + if (field?.contains('price') == true || + field?.contains('investment') == true || + field?.contains('rent') == true || + field?.contains('reserve') == true) { return '\$${NumberFormat('#,##0.00').format(value)}'; } else if (field?.contains('units') == true) { int units = value.round(); @@ -797,7 +880,7 @@ class _UpdatesPageState extends State { return NumberFormat('#,##0.00').format(value); } } - + return value.toString(); } diff --git a/lib/screens/lock_screen.dart b/lib/screens/lock_screen.dart index 5b31936..92c01bd 100644 --- a/lib/screens/lock_screen.dart +++ b/lib/screens/lock_screen.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:local_auth/local_auth.dart'; import 'package:realtoken_asset_tracker/services/biometric_service.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; @@ -85,7 +83,8 @@ class _LockScreenState extends State with WidgetsBindingObserver { }); try { - final authenticated = await _biometricService.authenticate(reason: 'Veuillez vous authentifier pour accéder à l\'application'); + final authenticated = await _biometricService.authenticate( + reason: 'Veuillez vous authentifier pour accéder à l\'application'); if (authenticated) { widget.onAuthenticated(); @@ -158,7 +157,9 @@ class _LockScreenState extends State with WidgetsBindingObserver { Text( 'RealToken App', style: TextStyle( - fontSize: 24 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 24 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.bold, ), ), @@ -167,7 +168,9 @@ class _LockScreenState extends State with WidgetsBindingObserver { S.of(context).pleaseAuthenticateToAccess, textAlign: TextAlign.center, style: TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 16 + + Provider.of(context, listen: false) + .getTextSizeOffset(), color: Colors.grey[600], ), ), @@ -181,9 +184,12 @@ class _LockScreenState extends State with WidgetsBindingObserver { ElevatedButton.icon( onPressed: _authenticate, icon: Icon(icon, size: 24), - label: Text(S.of(context).authenticateWithBiometric(_biometricType)), + label: Text(S + .of(context) + .authenticateWithBiometric(_biometricType)), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -195,13 +201,16 @@ class _LockScreenState extends State with WidgetsBindingObserver { S.of(context).biometricAuthenticationFailed, style: TextStyle( color: Colors.red, - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset(), + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset(), ), ), const SizedBox(height: 8), TextButton( onPressed: () => widget.onAuthenticated(), - child: Text(S.of(context).continueWithoutAuthentication), + child: Text( + S.of(context).continueWithoutAuthentication), ), ], ], diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index a61a78b..f33fded 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -5,70 +5,77 @@ import 'package:flutter/material.dart'; import 'package:realtoken_asset_tracker/utils/parameters.dart'; import 'package:realtoken_asset_tracker/utils/contracts_constants.dart'; import 'package:realtoken_asset_tracker/utils/performance_utils.dart'; -import 'package:realtoken_asset_tracker/utils/cache_constants.dart'; -import 'package:realtoken_asset_tracker/services/api_service_helpers.dart'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ApiService { // Constantes pour les timeouts améliorés - static const Duration _shortTimeout = Duration(seconds: 15); // Augmenté de 10 à 15 secondes - static const Duration _mediumTimeout = Duration(seconds: 30); // Augmenté de 20 à 30 secondes - static const Duration _longTimeout = Duration(seconds: 45); // Augmenté de 30 à 45 secondes + static const Duration _shortTimeout = + Duration(seconds: 15); // Augmenté de 10 à 15 secondes + static const Duration _mediumTimeout = + Duration(seconds: 30); // Augmenté de 20 à 30 secondes + static const Duration _longTimeout = + Duration(seconds: 45); // Augmenté de 30 à 45 secondes static const Duration _veryLongTimeout = Duration(minutes: 2); - + // Nouvelles constantes pour la stratégie de retry static const int _maxRetries = 2; static const Duration _retryDelay = Duration(seconds: 2); - + // Pool de clients HTTP réutilisables static final http.Client _httpClient = http.Client(); /// Méthode pour effectuer une requête HTTP avec retry automatique - static Future _httpGetWithRetry(String url, { + static Future _httpGetWithRetry( + String url, { Duration timeout = const Duration(seconds: 15), int maxRetries = _maxRetries, Duration retryDelay = _retryDelay, String? debugContext, }) async { int attempt = 0; - + while (attempt <= maxRetries) { try { if (attempt > 0) { - debugPrint("🔄 Tentative ${attempt + 1}/${maxRetries + 1} pour ${debugContext ?? 'requête'}"); + debugPrint( + "🔄 Tentative ${attempt + 1}/${maxRetries + 1} pour ${debugContext ?? 'requête'}"); await Future.delayed(retryDelay * attempt); // Délai progressif } - - final response = await _httpClient.get(Uri.parse(url)) - .timeout(timeout, onTimeout: () { - throw TimeoutException('Timeout après ${timeout.inSeconds}s pour ${debugContext ?? url}'); + + final response = await _httpClient.get(Uri.parse(url)).timeout(timeout, + onTimeout: () { + throw TimeoutException( + 'Timeout après ${timeout.inSeconds}s pour ${debugContext ?? url}'); }); - + return response; } catch (e) { attempt++; - + // Si c'est la dernière tentative ou si l'erreur n'est pas récupérable, relancer if (attempt > maxRetries || !_isRetryableError(e)) { - debugPrint("❌ Échec définitif ${debugContext ?? 'requête'} après $attempt tentatives: $e"); + debugPrint( + "❌ Échec définitif ${debugContext ?? 'requête'} après $attempt tentatives: $e"); rethrow; } - - debugPrint("⚠️ Tentative $attempt échouée pour ${debugContext ?? 'requête'}: $e"); + + debugPrint( + "⚠️ Tentative $attempt échouée pour ${debugContext ?? 'requête'}: $e"); } } - + throw Exception('Nombre maximum de tentatives atteint'); } - + /// Détermine si une erreur est récupérable avec un retry static bool _isRetryableError(dynamic error) { if (error is TimeoutException) return true; if (error is SocketException) return true; if (error is HttpException) return true; - if (error is FormatException) return false; // Erreur de format, pas de retry + if (error is FormatException) + return false; // Erreur de format, pas de retry if (error is http.ClientException) return true; return true; // Par défaut, on retry } @@ -88,12 +95,13 @@ class ApiService { int successCount = 0; int errorCount = 0; - debugPrint("🚀 Traitement parallèle $debugName pour ${wallets.length} wallets (max $maxConcurrentRequests simultanés)"); + debugPrint( + "🚀 Traitement parallèle $debugName pour ${wallets.length} wallets (max $maxConcurrentRequests simultanés)"); // Traiter les wallets par chunks pour éviter de surcharger le serveur for (int i = 0; i < wallets.length; i += maxConcurrentRequests) { final chunk = wallets.skip(i).take(maxConcurrentRequests).toList(); - + // Traitement parallèle du chunk actuel final futures = chunk.map((wallet) async { try { @@ -115,12 +123,13 @@ class ApiService { // Attendre que tous les wallets du chunk soient traités final chunkResults = await Future.wait(futures); - + // Ajouter les résultats non-null à la liste finale results.addAll(chunkResults.where((result) => result != null).cast()); - + processedCount += chunk.length; - debugPrint("📊 Progression $debugName: ${processedCount}/${wallets.length} wallets traités"); + debugPrint( + "📊 Progression $debugName: $processedCount/${wallets.length} wallets traités"); // Petite pause entre les chunks pour être gentil avec le serveur if (i + maxConcurrentRequests < wallets.length) { @@ -128,7 +137,8 @@ class ApiService { } } - debugPrint("✅ Traitement parallèle $debugName terminé: $successCount réussis, $errorCount erreurs"); + debugPrint( + "✅ Traitement parallèle $debugName terminé: $successCount réussis, $errorCount erreurs"); return results; } @@ -158,9 +168,10 @@ class ApiService { if (cachedData == null && alternativeCacheKey != null) { cachedData = box.get(alternativeCacheKey); } - + if (cachedData != null) { - cachedResult = fromJson(cachedData is String ? jsonDecode(cachedData) : cachedData); + cachedResult = fromJson( + cachedData is String ? jsonDecode(cachedData) : cachedData); debugPrint("🔵 Cache $debugName disponible"); } } catch (e) { @@ -195,11 +206,12 @@ class ApiService { try { debugPrint("🔄 Mise à jour $debugName depuis l'API..."); final apiResult = await apiCall(); - + if (apiResult != null && apiResult != emptyValue) { // Sauvegarder le nouveau cache final jsonData = toJson(apiResult); - await box.put(cacheKey, jsonData is String ? jsonData : jsonEncode(jsonData)); + await box.put( + cacheKey, jsonData is String ? jsonData : jsonEncode(jsonData)); await box.put('lastFetchTime_$cacheKey', now.toIso8601String()); await box.put('lastExecutionTime_$debugName', now.toIso8601String()); debugPrint("💾 $debugName mis à jour depuis l'API"); @@ -218,7 +230,8 @@ class ApiService { } // 7. Dernier recours : valeur par défaut - debugPrint("❌ Aucune donnée disponible pour $debugName, utilisation valeur par défaut"); + debugPrint( + "❌ Aucune donnée disponible pour $debugName, utilisation valeur par défaut"); return emptyValue; } @@ -247,7 +260,8 @@ class ApiService { } /// Récupère toutes les adresses associées à une adresse Ethereum via FastAPI - static Future?> fetchUserAndAddresses(String address) async { + static Future?> fetchUserAndAddresses( + String address) async { final apiUrl = "${Parameters.mainApiUrl}/wallet_userId/$address"; debugPrint("📡 Envoi de la requête à FastAPI: $apiUrl"); @@ -284,7 +298,8 @@ class ApiService { } // Méthode factorisée pour fetch les tokens depuis The Graph avec cache optimisé - static Future> fetchWalletTokens({bool forceFetch = false}) async { + static Future> fetchWalletTokens( + {bool forceFetch = false}) async { final prefs = await SharedPreferences.getInstance(); List evmAddresses = prefs.getStringList('evmAddresses') ?? []; @@ -316,14 +331,16 @@ class ApiService { if (response.statusCode == 200) { final walletData = jsonDecode(response.body); if (walletData is List && walletData.isNotEmpty) { - debugPrint("✅ ${walletData.length} tokens récupérés pour le wallet $wallet"); + debugPrint( + "✅ ${walletData.length} tokens récupérés pour le wallet $wallet"); return walletData; } else { debugPrint("⚠️ Aucun token trouvé pour le wallet $wallet"); return []; } } else { - debugPrint("❌ Erreur récupération tokens wallet $wallet: Code HTTP ${response.statusCode}"); + debugPrint( + "❌ Erreur récupération tokens wallet $wallet: Code HTTP ${response.statusCode}"); return null; // Sera filtré par _processWalletsInParallel } }, @@ -335,18 +352,20 @@ class ApiService { allWalletTokens.addAll(tokenList); } - debugPrint("📊 Récapitulatif: ${allWalletTokens.length} tokens récupérés au total"); + debugPrint( + "📊 Récapitulatif: ${allWalletTokens.length} tokens récupérés au total"); return allWalletTokens; }, ); } // Récupérer la liste complète des RealTokens depuis l'API pitswap avec cache optimisé - static Future> fetchRealTokens({bool forceFetch = false}) async { + static Future> fetchRealTokens( + {bool forceFetch = false}) async { debugPrint("🚀 apiService: fetchRealTokens -> Lancement de la requête"); final box = Hive.box('realTokens'); - + return _fetchWithCacheList( cacheKey: 'cachedRealTokens', debugName: "RealTokens", @@ -354,22 +373,26 @@ class ApiService { shouldUpdate: () async { // Logique spécifique : vérifier les timestamps serveur if (forceFetch) return true; - + try { final lastUpdateTime = box.get('lastUpdateTime_RealTokens'); if (lastUpdateTime == null) return true; // Vérification de la dernière mise à jour sur le serveur - final lastUpdateResponse = await http.get( - Uri.parse('${Parameters.realTokensUrl}/last_get_realTokens_mobileapps') - ).timeout(Duration(seconds: 10)); + final lastUpdateResponse = await http + .get(Uri.parse( + '${Parameters.realTokensUrl}/last_get_realTokens_mobileapps')) + .timeout(Duration(seconds: 10)); if (lastUpdateResponse.statusCode == 200) { - final String lastUpdateDateString = json.decode(lastUpdateResponse.body); - final DateTime lastUpdateDate = DateTime.parse(lastUpdateDateString); + final String lastUpdateDateString = + json.decode(lastUpdateResponse.body); + final DateTime lastUpdateDate = + DateTime.parse(lastUpdateDateString); final DateTime lastExecutionDate = DateTime.parse(lastUpdateTime); - - bool needsUpdate = !lastExecutionDate.isAtSameMomentAs(lastUpdateDate); + + bool needsUpdate = + !lastExecutionDate.isAtSameMomentAs(lastUpdateDate); if (!needsUpdate) { debugPrint("✅ Données RealTokens déjà à jour selon le serveur"); } @@ -382,31 +405,34 @@ class ApiService { }, apiCall: () async { // Récupérer les nouvelles données - final response = await http.get( - Uri.parse('${Parameters.realTokensUrl}/realTokens_mobileapps') - ).timeout(Duration(seconds: 30)); + final response = await http + .get(Uri.parse('${Parameters.realTokensUrl}/realTokens_mobileapps')) + .timeout(Duration(seconds: 30)); if (response.statusCode == 200) { final data = json.decode(response.body); - + // Sauvegarder le timestamp serveur spécifique à RealTokens try { - final lastUpdateResponse = await http.get( - Uri.parse('${Parameters.realTokensUrl}/last_get_realTokens_mobileapps') - ).timeout(Duration(seconds: 5)); - + final lastUpdateResponse = await http + .get(Uri.parse( + '${Parameters.realTokensUrl}/last_get_realTokens_mobileapps')) + .timeout(Duration(seconds: 5)); + if (lastUpdateResponse.statusCode == 200) { - final String lastUpdateDateString = json.decode(lastUpdateResponse.body); + final String lastUpdateDateString = + json.decode(lastUpdateResponse.body); await box.put('lastUpdateTime_RealTokens', lastUpdateDateString); } } catch (e) { debugPrint("⚠️ Erreur sauvegarde timestamp RealTokens: $e"); } - + debugPrint("💾 RealTokens mis à jour: ${data.length} tokens"); return data; } else { - throw Exception("Erreur HTTP ${response.statusCode} lors de la récupération des RealTokens"); + throw Exception( + "Erreur HTTP ${response.statusCode} lors de la récupération des RealTokens"); } }, ); @@ -415,7 +441,7 @@ class ApiService { // Récupérer la liste complète des offres YAM depuis l'API avec cache optimisé static Future> fetchYamMarket({bool forceFetch = false}) async { final box = Hive.box('realTokens'); - + return _fetchWithCacheList( cacheKey: 'cachedYamMarket', debugName: "YAM Market", @@ -423,22 +449,26 @@ class ApiService { shouldUpdate: () async { // Logique spécifique : vérifier les timestamps serveur YAM if (forceFetch) return true; - + try { final lastUpdateTime = box.get('lastUpdateTime_YamMarket'); if (lastUpdateTime == null) return true; // Vérification de la dernière mise à jour sur le serveur - final lastUpdateResponse = await http.get( - Uri.parse('${Parameters.realTokensUrl}/last_update_yam_offers_mobileapps') - ).timeout(Duration(seconds: 10)); + final lastUpdateResponse = await http + .get(Uri.parse( + '${Parameters.realTokensUrl}/last_update_yam_offers_mobileapps')) + .timeout(Duration(seconds: 10)); if (lastUpdateResponse.statusCode == 200) { - final String lastUpdateDateString = json.decode(lastUpdateResponse.body); - final DateTime lastUpdateDate = DateTime.parse(lastUpdateDateString); + final String lastUpdateDateString = + json.decode(lastUpdateResponse.body); + final DateTime lastUpdateDate = + DateTime.parse(lastUpdateDateString); final DateTime lastExecutionDate = DateTime.parse(lastUpdateTime); - - bool needsUpdate = !lastExecutionDate.isAtSameMomentAs(lastUpdateDate); + + bool needsUpdate = + !lastExecutionDate.isAtSameMomentAs(lastUpdateDate); if (!needsUpdate) { debugPrint("✅ Données YAM Market déjà à jour selon le serveur"); } @@ -451,38 +481,43 @@ class ApiService { }, apiCall: () async { // Récupérer les nouvelles données YAM - final response = await http.get( - Uri.parse('${Parameters.realTokensUrl}/get_yam_offers_mobileapps') - ).timeout(Duration(seconds: 30)); + final response = await http + .get(Uri.parse( + '${Parameters.realTokensUrl}/get_yam_offers_mobileapps')) + .timeout(Duration(seconds: 30)); if (response.statusCode == 200) { final data = json.decode(response.body); - + // Sauvegarder le timestamp serveur spécifique à YAM Market try { - final lastUpdateResponse = await http.get( - Uri.parse('${Parameters.realTokensUrl}/last_update_yam_offers_mobileapps') - ).timeout(Duration(seconds: 5)); - + final lastUpdateResponse = await http + .get(Uri.parse( + '${Parameters.realTokensUrl}/last_update_yam_offers_mobileapps')) + .timeout(Duration(seconds: 5)); + if (lastUpdateResponse.statusCode == 200) { - final String lastUpdateDateString = json.decode(lastUpdateResponse.body); + final String lastUpdateDateString = + json.decode(lastUpdateResponse.body); await box.put('lastUpdateTime_YamMarket', lastUpdateDateString); } } catch (e) { debugPrint("⚠️ Erreur sauvegarde timestamp YAM Market: $e"); } - + debugPrint("💾 YAM Market mis à jour: ${data.length} offres"); return data; } else { - throw Exception("Erreur HTTP ${response.statusCode} lors de la récupération du YAM Market"); + throw Exception( + "Erreur HTTP ${response.statusCode} lors de la récupération du YAM Market"); } }, ); } // Récupérer les données de loyer pour chaque wallet et les fusionner avec cache - static Future>> fetchRentData({bool forceFetch = false}) async { + static Future>> fetchRentData( + {bool forceFetch = false}) async { SharedPreferences prefs = await SharedPreferences.getInstance(); List wallets = prefs.getStringList('evmAddresses') ?? []; @@ -492,26 +527,32 @@ class ApiService { final box = Hive.box('realTokens'); final DateTime now = DateTime.now(); - + // Calculer le début de la semaine actuelle (lundi) - final DateTime startOfCurrentWeek = now.subtract(Duration(days: now.weekday - 1)); - final DateTime startOfCurrentWeekMidnight = DateTime(startOfCurrentWeek.year, startOfCurrentWeek.month, startOfCurrentWeek.day); - + final DateTime startOfCurrentWeek = + now.subtract(Duration(days: now.weekday - 1)); + final DateTime startOfCurrentWeekMidnight = DateTime( + startOfCurrentWeek.year, + startOfCurrentWeek.month, + startOfCurrentWeek.day); + // TOUJOURS commencer par charger les données existantes de tous les wallets debugPrint("📦 Chargement des données existantes pour tous les wallets"); List> mergedRentData = []; await _loadRentDataFromCache(box, wallets).then((cachedData) { mergedRentData.addAll(cachedData); - debugPrint("📦 ${mergedRentData.length} entrées chargées depuis le cache"); - + debugPrint( + "📦 ${mergedRentData.length} entrées chargées depuis le cache"); + // Diagnostic anti-doublons : vérifier les totaux double totalRentFromCache = 0; for (var entry in mergedRentData) { totalRentFromCache += (entry['rent'] ?? 0).toDouble(); } - debugPrint("📊 Total rent depuis cache: \$${totalRentFromCache.toStringAsFixed(2)}"); + debugPrint( + "📊 Total rent depuis cache: \$${totalRentFromCache.toStringAsFixed(2)}"); }); - + // Vérifier si une réponse 429 a été reçue récemment final last429Time = box.get('lastRent429Time'); if (last429Time != null && !forceFetch) { @@ -529,7 +570,7 @@ class ApiService { final lastSuccessTime = box.get(lastSuccessKey); final cacheKey = 'cachedRentData_$wallet'; final cachedData = box.get(cacheKey); - + if (lastSuccessTime == null || cachedData == null) { debugPrint("❌ Wallet $wallet: pas de succès récent ou cache manquant"); allWalletsProcessed = false; @@ -541,7 +582,7 @@ class ApiService { allWalletsProcessed = false; break; } - + // Vérifier que le cache n'est pas vide ou corrompu try { final List cacheContent = json.decode(cachedData); @@ -557,7 +598,7 @@ class ApiService { } } } - + // Vérifier si la dernière mise à jour réussie est trop ancienne (plus de 7 jours) final lastSuccessfulFetch = box.get('lastSuccessfulRentFetch'); bool isDataTooOld = false; @@ -567,24 +608,28 @@ class ApiService { } else { isDataTooOld = true; // Pas de fetch réussi enregistré } - + // Si tous les wallets sont traités ET qu'on n'est pas mardi ET pas de forceFetch ET que les données ne sont pas trop anciennes, utiliser le cache final bool isTuesday = now.weekday == DateTime.tuesday; if (allWalletsProcessed && !isTuesday && !forceFetch && !isDataTooOld) { - debugPrint("🛑 Tous les wallets traités cette semaine, utilisation des données existantes"); + debugPrint( + "🛑 Tous les wallets traités cette semaine, utilisation des données existantes"); return mergedRentData; } - + if (isDataTooOld) { - debugPrint("⏰ Données trop anciennes (>7 jours), forçage de la mise à jour"); + debugPrint( + "⏰ Données trop anciennes (>7 jours), forçage de la mise à jour"); } - - debugPrint("🔄 Certains wallets non traités ou c'est mardi, traitement nécessaire"); + + debugPrint( + "🔄 Certains wallets non traités ou c'est mardi, traitement nécessaire"); // Sauvegarder les données existantes comme backup final Map>> existingDataByWallet = {}; for (String wallet in wallets) { - existingDataByWallet[wallet] = await _loadRentDataFromCacheForWallet(box, wallet); + existingDataByWallet[wallet] = + await _loadRentDataFromCacheForWallet(box, wallet); } List walletsToProcess = []; @@ -596,7 +641,7 @@ class ApiService { final lastSuccessTime = box.get(lastSuccessKey); final cacheKey = 'cachedRentData_$wallet'; final cachedData = box.get(cacheKey); - + if (lastSuccessTime != null && cachedData != null && !forceFetch) { final DateTime lastSuccess = DateTime.parse(lastSuccessTime); if (lastSuccess.isAfter(startOfCurrentWeekMidnight)) { @@ -604,26 +649,30 @@ class ApiService { try { final List cacheContent = json.decode(cachedData); if (cacheContent.isNotEmpty) { - debugPrint("✅ Wallet $wallet déjà traité cette semaine avec cache valide"); + debugPrint( + "✅ Wallet $wallet déjà traité cette semaine avec cache valide"); successfulWallets.add(wallet); continue; } else { - debugPrint("⚠️ Wallet $wallet: cache vide, retraitement nécessaire"); + debugPrint( + "⚠️ Wallet $wallet: cache vide, retraitement nécessaire"); } } catch (e) { - debugPrint("⚠️ Wallet $wallet: cache corrompu, retraitement nécessaire - $e"); + debugPrint( + "⚠️ Wallet $wallet: cache corrompu, retraitement nécessaire - $e"); } } } walletsToProcess.add(wallet); } - debugPrint("🚀 ${walletsToProcess.length} wallets à traiter, ${successfulWallets.length} déjà traités"); + debugPrint( + "🚀 ${walletsToProcess.length} wallets à traiter, ${successfulWallets.length} déjà traités"); // Traiter les wallets restants un par un for (String wallet in walletsToProcess) { final url = '${Parameters.rentTrackerUrl}/rent_holder/$wallet'; - + try { debugPrint("🔄 Traitement du wallet: $wallet"); final response = await _httpGetWithRetry( @@ -633,7 +682,8 @@ class ApiService { ); if (response.statusCode == 429) { - debugPrint('⚠️ 429 Too Many Requests pour le wallet $wallet - conservation des données existantes'); + debugPrint( + '⚠️ 429 Too Many Requests pour le wallet $wallet - conservation des données existantes'); await box.put('lastRent429Time', now.toIso8601String()); break; // Arrêter le traitement mais conserver les données existantes } @@ -641,41 +691,44 @@ class ApiService { if (response.statusCode == 200) { debugPrint("✅ RentTracker, requête réussie pour $wallet"); - List> rentData = List>.from( - json.decode(response.body) - ); - + List> rentData = + List>.from(json.decode(response.body)); + // Retirer TOUTES les anciennes données de ce wallet du merge global // (on ne peut pas se baser sur les montants car ils peuvent avoir changé) - Set walletDates = Set(); + Set walletDates = {}; if (existingDataByWallet[wallet] != null) { for (var existing in existingDataByWallet[wallet]!) { walletDates.add(existing['date']); } } - + // Supprimer toutes les entrées correspondant aux dates de ce wallet - mergedRentData.removeWhere((entry) => walletDates.contains(entry['date'])); - + mergedRentData + .removeWhere((entry) => walletDates.contains(entry['date'])); + // Traiter et ajouter les nouvelles données List> processedData = []; - Map walletDateRentMap = {}; // Éviter les doublons pour ce wallet - + Map walletDateRentMap = + {}; // Éviter les doublons pour ce wallet + for (var rentEntry in rentData) { DateTime rentDate = DateTime.parse(rentEntry['date']); rentDate = rentDate.add(Duration(days: 1)); - String updatedDate = "${rentDate.year}-${rentDate.month.toString().padLeft(2, '0')}-${rentDate.day.toString().padLeft(2, '0')}"; + String updatedDate = + "${rentDate.year}-${rentDate.month.toString().padLeft(2, '0')}-${rentDate.day.toString().padLeft(2, '0')}"; // Cumuler les rents pour la même date dans ce wallet double rentAmount = (rentEntry['rent'] ?? 0).toDouble(); - walletDateRentMap[updatedDate] = (walletDateRentMap[updatedDate] ?? 0) + rentAmount; + walletDateRentMap[updatedDate] = + (walletDateRentMap[updatedDate] ?? 0) + rentAmount; } - + // Ajouter les nouvelles données consolidées au merge global for (var entry in walletDateRentMap.entries) { String date = entry.key; double walletRentForDate = entry.value; - + // Vérifier s'il existe déjà une entrée pour cette date (autres wallets) final existingEntry = mergedRentData.firstWhere( (entry) => entry['date'] == date, @@ -684,7 +737,8 @@ class ApiService { if (existingEntry.isNotEmpty) { // Ajouter le rent de ce wallet au total existant (autres wallets) - existingEntry['rent'] = (existingEntry['rent'] ?? 0) + walletRentForDate; + existingEntry['rent'] = + (existingEntry['rent'] ?? 0) + walletRentForDate; } else { // Créer une nouvelle entrée pour cette date mergedRentData.add({ @@ -692,7 +746,7 @@ class ApiService { 'rent': walletRentForDate, }); } - + // Sauvegarder les données brutes pour le cache par wallet processedData.add({ 'date': date, @@ -701,27 +755,31 @@ class ApiService { } // Sauvegarder le cache pour ce wallet avec vérification - final saveSuccess = await _safeCacheSave(box, 'cachedRentData_$wallet', processedData); + final saveSuccess = await _safeCacheSave( + box, 'cachedRentData_$wallet', processedData); if (saveSuccess) { await box.put('lastRentSuccess_$wallet', now.toIso8601String()); } else { - debugPrint('⚠️ Échec sauvegarde cache pour $wallet, tentative de repli'); + debugPrint( + '⚠️ Échec sauvegarde cache pour $wallet, tentative de repli'); // Tentative de repli sans utiliser _safeCacheSave try { - await box.put('cachedRentData_$wallet', json.encode(processedData)); + await box.put( + 'cachedRentData_$wallet', json.encode(processedData)); await box.put('lastRentSuccess_$wallet', now.toIso8601String()); } catch (e) { debugPrint('❌ Échec total sauvegarde pour $wallet: $e'); } } successfulWallets.add(wallet); - } else { - debugPrint('❌ Erreur HTTP ${response.statusCode} pour le wallet: $wallet - conservation des données existantes'); + debugPrint( + '❌ Erreur HTTP ${response.statusCode} pour le wallet: $wallet - conservation des données existantes'); // Les données existantes sont déjà dans mergedRentData, ne rien faire } } catch (e) { - debugPrint('❌ Exception pour le wallet $wallet: $e - conservation des données existantes'); + debugPrint( + '❌ Exception pour le wallet $wallet: $e - conservation des données existantes'); // Les données existantes sont déjà dans mergedRentData, ne rien faire } } @@ -732,13 +790,15 @@ class ApiService { // Sauvegarder le cache global TOUJOURS (même en cas d'erreur partielle) await box.put('cachedRentData', json.encode(mergedRentData)); await box.put('lastRentFetchTime', now.toIso8601String()); - + // Marquer comme succès complet seulement si tous les wallets ont été traités if (successfulWallets.length == wallets.length) { await box.put('lastSuccessfulRentFetch', now.toIso8601String()); - debugPrint("✅ Succès complet: ${mergedRentData.length} entrées (${successfulWallets.length}/${wallets.length} wallets)"); + debugPrint( + "✅ Succès complet: ${mergedRentData.length} entrées (${successfulWallets.length}/${wallets.length} wallets)"); } else { - debugPrint("⚠️ Succès partiel: ${mergedRentData.length} entrées (${successfulWallets.length}/${wallets.length} wallets)"); + debugPrint( + "⚠️ Succès partiel: ${mergedRentData.length} entrées (${successfulWallets.length}/${wallets.length} wallets)"); } // Diagnostic final anti-doublons @@ -752,7 +812,8 @@ class ApiService { } /// Charge les données de loyer depuis le cache pour tous les wallets - static Future>> _loadRentDataFromCache(Box box, List wallets) async { + static Future>> _loadRentDataFromCache( + Box box, List wallets) async { // Essayer le cache global d'abord final globalCache = box.get('cachedRentData'); if (globalCache != null) { @@ -787,11 +848,13 @@ class ApiService { } /// Charge les données de loyer depuis le cache pour un wallet spécifique - static Future>> _loadRentDataFromCacheForWallet(Box box, String wallet) async { + static Future>> _loadRentDataFromCacheForWallet( + Box box, String wallet) async { return await _safeLoadWalletCache(box, wallet); } - static Future>> fetchWhitelistTokens({bool forceFetch = false}) async { + static Future>> fetchWhitelistTokens( + {bool forceFetch = false}) async { SharedPreferences prefs = await SharedPreferences.getInstance(); List wallets = prefs.getStringList('evmAddresses') ?? []; @@ -811,49 +874,54 @@ class ApiService { final DateTime now = DateTime.now(); List> mergedWhitelistTokens = []; - debugPrint("🚀 Récupération des tokens whitelistés pour ${wallets.length} wallets"); + debugPrint( + "🚀 Récupération des tokens whitelistés pour ${wallets.length} wallets"); // Parcourir chaque wallet pour récupérer ses tokens whitelistés for (String wallet in wallets) { final url = '${Parameters.rentTrackerUrl}/whitelist2/$wallet'; - + try { - final response = await http.get(Uri.parse(url)) - .timeout(Duration(seconds: 15)); + final response = + await http.get(Uri.parse(url)).timeout(Duration(seconds: 15)); // En cas de code 429, sauvegarder l'heure et interrompre la boucle if (response.statusCode == 429) { debugPrint('⚠️ 429 Too Many Requests pour wallet: $wallet'); await box.put('lastWhitelistFetchTime', now.toIso8601String()); - throw Exception("Limite de requêtes atteinte pour les tokens whitelistés"); + throw Exception( + "Limite de requêtes atteinte pour les tokens whitelistés"); } if (response.statusCode == 200) { debugPrint("✅ Requête réussie pour wallet: $wallet"); - List> whitelistData = List>.from( - json.decode(response.body) - ); + List> whitelistData = + List>.from(json.decode(response.body)); mergedWhitelistTokens.addAll(whitelistData); } else { - debugPrint('❌ Erreur HTTP ${response.statusCode} pour wallet: $wallet'); - throw Exception('Impossible de récupérer les tokens whitelistés pour wallet: $wallet'); + debugPrint( + '❌ Erreur HTTP ${response.statusCode} pour wallet: $wallet'); + throw Exception( + 'Impossible de récupérer les tokens whitelistés pour wallet: $wallet'); } } catch (e) { debugPrint('❌ Exception pour wallet $wallet: $e'); - throw e; + rethrow; } } // Sauvegarder le timestamp spécifique pour les tokens whitelistés await box.put('lastWhitelistFetchTime', now.toIso8601String()); - debugPrint("✅ ${mergedWhitelistTokens.length} tokens whitelistés récupérés"); + debugPrint( + "✅ ${mergedWhitelistTokens.length} tokens whitelistés récupérés"); return mergedWhitelistTokens; }, ); } - static Future> fetchCurrencies({bool forceFetch = false}) async { + static Future> fetchCurrencies( + {bool forceFetch = false}) async { return _fetchWithCache>( cacheKey: 'cachedCurrencies', debugName: "Currencies", @@ -864,25 +932,29 @@ class ApiService { emptyValue: {}, apiCall: () async { debugPrint("🔄 Récupération des devises depuis CoinGecko"); - - final response = await http.get(Uri.parse(Parameters.coingeckoUrl)) + + final response = await http + .get(Uri.parse(Parameters.coingeckoUrl)) .timeout(Duration(seconds: 15)); if (response.statusCode == 200) { final data = json.decode(response.body); - final currencies = data['market_data']['current_price'] as Map; - + final currencies = + data['market_data']['current_price'] as Map; + debugPrint("✅ ${currencies.length} devises récupérées"); return currencies; } else { - throw Exception('Erreur HTTP ${response.statusCode} lors de la récupération des devises'); + throw Exception( + 'Erreur HTTP ${response.statusCode} lors de la récupération des devises'); } }, ); } // Récupérer le userId associé à une adresse Ethereum - static Future>> fetchRmmBalances({bool forceFetch = false}) async { + static Future>> fetchRmmBalances( + {bool forceFetch = false}) async { final prefs = await SharedPreferences.getInstance(); List evmAddresses = prefs.getStringList('evmAddresses') ?? []; @@ -895,35 +967,46 @@ class ApiService { cacheKey: 'cachedRmmBalances', debugName: "RMM Balances", forceFetch: forceFetch, - customCacheDuration: Duration(minutes: 15), // Cache plus court pour les balances + customCacheDuration: + Duration(minutes: 15), // Cache plus court pour les balances fromJson: (data) => List>.from(data), toJson: (data) => data, emptyValue: >[], apiCall: () async { // Utilisation des constantes centralisées - const String usdcDepositContract = ContractsConstants.usdcDepositContract; + const String usdcDepositContract = + ContractsConstants.usdcDepositContract; const String usdcBorrowContract = ContractsConstants.usdcBorrowContract; - const String xdaiDepositContract = ContractsConstants.xdaiDepositContract; + const String xdaiDepositContract = + ContractsConstants.xdaiDepositContract; const String xdaiBorrowContract = ContractsConstants.xdaiBorrowContract; const String gnosisUsdcContract = ContractsConstants.gnosisUsdcContract; const String gnosisRegContract = ContractsConstants.gnosisRegContract; - const String gnosisVaultRegContract = ContractsConstants.gnosisVaultRegContract; + const String gnosisVaultRegContract = + ContractsConstants.gnosisVaultRegContract; List> allBalances = []; - debugPrint("🔄 Récupération des balances RMM pour ${evmAddresses.length} wallets"); + debugPrint( + "🔄 Récupération des balances RMM pour ${evmAddresses.length} wallets"); for (var address in evmAddresses) { try { // Requêtes pour tous les contrats final futures = await Future.wait([ - _fetchBalance(usdcDepositContract, address, forceFetch: forceFetch), - _fetchBalance(usdcBorrowContract, address, forceFetch: forceFetch), - _fetchBalance(xdaiDepositContract, address, forceFetch: forceFetch), - _fetchBalance(xdaiBorrowContract, address, forceFetch: forceFetch), - _fetchBalance(gnosisUsdcContract, address, forceFetch: forceFetch), + _fetchBalance(usdcDepositContract, address, + forceFetch: forceFetch), + _fetchBalance(usdcBorrowContract, address, + forceFetch: forceFetch), + _fetchBalance(xdaiDepositContract, address, + forceFetch: forceFetch), + _fetchBalance(xdaiBorrowContract, address, + forceFetch: forceFetch), + _fetchBalance(gnosisUsdcContract, address, + forceFetch: forceFetch), _fetchBalance(gnosisRegContract, address, forceFetch: forceFetch), - _fetchVaultBalance(gnosisVaultRegContract, address, forceFetch: forceFetch), + _fetchVaultBalance(gnosisVaultRegContract, address, + forceFetch: forceFetch), _fetchNativeBalance(address, forceFetch: forceFetch), ]); @@ -939,21 +1022,31 @@ class ApiService { ] = futures; // Vérification que toutes les requêtes ont retourné une valeur - if (usdcDepositResponse != null && usdcBorrowResponse != null && - xdaiDepositResponse != null && xdaiBorrowResponse != null && - gnosisUsdcResponse != null && gnosisXdaiResponse != null) { - + if (usdcDepositResponse != null && + usdcBorrowResponse != null && + xdaiDepositResponse != null && + xdaiBorrowResponse != null && + gnosisUsdcResponse != null && + gnosisXdaiResponse != null) { final timestamp = DateTime.now().toIso8601String(); // Conversion optimisée des balances en double - double usdcDepositBalance = PerformanceUtils.bigIntToDouble(usdcDepositResponse, 6); - double usdcBorrowBalance = PerformanceUtils.bigIntToDouble(usdcBorrowResponse, 6); - double xdaiDepositBalance = PerformanceUtils.bigIntToDouble(xdaiDepositResponse, 18); - double xdaiBorrowBalance = PerformanceUtils.bigIntToDouble(xdaiBorrowResponse, 18); - double gnosisUsdcBalance = PerformanceUtils.bigIntToDouble(gnosisUsdcResponse, 6); - double gnosisRegBalance = PerformanceUtils.bigIntToDouble(gnosisRegResponse ?? BigInt.zero, 18); - double gnosisVaultRegBalance = PerformanceUtils.bigIntToDouble(gnosisVaultRegResponse ?? BigInt.zero, 18); - double gnosisXdaiBalance = PerformanceUtils.bigIntToDouble(gnosisXdaiResponse, 18); + double usdcDepositBalance = + PerformanceUtils.bigIntToDouble(usdcDepositResponse, 6); + double usdcBorrowBalance = + PerformanceUtils.bigIntToDouble(usdcBorrowResponse, 6); + double xdaiDepositBalance = + PerformanceUtils.bigIntToDouble(xdaiDepositResponse, 18); + double xdaiBorrowBalance = + PerformanceUtils.bigIntToDouble(xdaiBorrowResponse, 18); + double gnosisUsdcBalance = + PerformanceUtils.bigIntToDouble(gnosisUsdcResponse, 6); + double gnosisRegBalance = PerformanceUtils.bigIntToDouble( + gnosisRegResponse ?? BigInt.zero, 18); + double gnosisVaultRegBalance = PerformanceUtils.bigIntToDouble( + gnosisVaultRegResponse ?? BigInt.zero, 18); + double gnosisXdaiBalance = + PerformanceUtils.bigIntToDouble(gnosisXdaiResponse, 18); // Ajout des données dans la liste allBalances.add({ @@ -976,7 +1069,7 @@ class ApiService { } } catch (e) { debugPrint("❌ Exception balances pour wallet $address: $e"); - throw e; + rethrow; } } @@ -987,9 +1080,11 @@ class ApiService { } /// Fonction pour récupérer le solde d'un token ERC20 (via eth_call) - static Future _fetchBalance(String contract, String address, {bool forceFetch = false}) async { + static Future _fetchBalance(String contract, String address, + {bool forceFetch = false}) async { final String cacheKey = 'cachedBalance_${contract}_$address'; - final box = await Hive.openBox('balanceCache'); // Remplacez par le système de stockage persistant que vous utilisez + final box = await Hive.openBox( + 'balanceCache'); // Remplacez par le système de stockage persistant que vous utilisez final now = DateTime.now(); // Récupérer l'heure de la dernière requête dans le cache @@ -1002,7 +1097,8 @@ class ApiService { // Vérifier si le résultat est mis en cache final cachedData = box.get(cacheKey); if (cachedData != null) { - debugPrint("🛑 apiService: fetchBallance -> Requete annulée, temps minimum pas atteint"); + debugPrint( + "🛑 apiService: fetchBallance -> Requete annulée, temps minimum pas atteint"); return BigInt.tryParse(cachedData); } } @@ -1016,7 +1112,10 @@ class ApiService { "jsonrpc": "2.0", "method": "eth_call", "params": [ - {"to": contract, "data": "0x70a08231000000000000000000000000${address.substring(2)}"}, + { + "to": contract, + "data": "0x70a08231000000000000000000000000${address.substring(2)}" + }, "latest" ], "id": 1 @@ -1032,24 +1131,28 @@ class ApiService { final balance = BigInt.parse(result.substring(2), radix: 16); // Sauvegarder le résultat dans le cache - debugPrint("🚀 apiService: RPC gnosis -> $contract balance récupérée: $balance"); + debugPrint( + "🚀 apiService: RPC gnosis -> $contract balance récupérée: $balance"); await box.put(cacheKey, balance.toString()); await box.put('lastFetchTime_$cacheKey', now.toIso8601String()); box.put('lastExecutionTime_Balances', now.toIso8601String()); return balance; } else { - debugPrint("apiService: RPC gnosis -> Invalid response for contract $contract: $result"); + debugPrint( + "apiService: RPC gnosis -> Invalid response for contract $contract: $result"); } } else { - debugPrint('apiService: RPC gnosis -> Failed to fetch balance for contract $contract. Status code: ${response.statusCode}'); + debugPrint( + 'apiService: RPC gnosis -> Failed to fetch balance for contract $contract. Status code: ${response.statusCode}'); } return null; } /// Fonction pour récupérer le solde du token natif (xDAI) via eth_getBalance - static Future _fetchNativeBalance(String address, {bool forceFetch = false}) async { + static Future _fetchNativeBalance(String address, + {bool forceFetch = false}) async { final String cacheKey = 'cachedNativeBalance_$address'; final box = await Hive.openBox('balanceCache'); final now = DateTime.now(); @@ -1093,103 +1196,119 @@ class ApiService { return null; } -static Future _fetchVaultBalance(String contract, String address, {bool forceFetch = false}) async { - final String cacheKey = 'cachedVaultBalance_${contract}_$address'; - final box = await Hive.openBox('balanceCache'); - final now = DateTime.now(); + static Future _fetchVaultBalance(String contract, String address, + {bool forceFetch = false}) async { + final String cacheKey = 'cachedVaultBalance_${contract}_$address'; + final box = await Hive.openBox('balanceCache'); + final now = DateTime.now(); - final String? lastFetchTime = box.get('lastFetchTime_$cacheKey'); + final String? lastFetchTime = box.get('lastFetchTime_$cacheKey'); - if (!forceFetch && lastFetchTime != null) { - final DateTime lastFetch = DateTime.parse(lastFetchTime); - if (now.difference(lastFetch) < Parameters.apiCacheDuration) { - final cachedData = box.get(cacheKey); - if (cachedData != null) { - debugPrint("🛑 apiService: fetchVaultBalance -> Requête annulée, cache valide"); - return BigInt.tryParse(cachedData); + if (!forceFetch && lastFetchTime != null) { + final DateTime lastFetch = DateTime.parse(lastFetchTime); + if (now.difference(lastFetch) < Parameters.apiCacheDuration) { + final cachedData = box.get(cacheKey); + if (cachedData != null) { + debugPrint( + "🛑 apiService: fetchVaultBalance -> Requête annulée, cache valide"); + return BigInt.tryParse(cachedData); + } } } - } - // Construire la data : 0xf262a083 + adresse paddée (sans '0x', alignée sur 32 bytes) - final String functionSelector = 'f262a083'; - final String paddedAddress = address.toLowerCase().replaceFirst('0x', '').padLeft(64, '0'); - final String data = '0x$functionSelector$paddedAddress'; - - final response = await http.post( - Uri.parse('https://rpc.gnosischain.com'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - "jsonrpc": "2.0", - "method": "eth_call", - "params": [ - {"to": contract, "data": data}, - "latest" - ], - "id": 1 - }), - ); - - if (response.statusCode == 200) { - final responseBody = json.decode(response.body); - final result = responseBody['result']; - - debugPrint("🚀 apiService: fetchVaultBalance -> Requête lancée"); - - if (result != null && result != "0x" && result.length >= 66) { - // On suppose que le solde est dans le 1er mot (64 caractères hex après le "0x") - final String balanceHex = result.substring(2, 66); - final balance = BigInt.parse(balanceHex, radix: 16); - - debugPrint("✅ apiService: fetchVaultBalance -> Balance récupérée: $balance"); - await box.put(cacheKey, balance.toString()); - await box.put('lastFetchTime_$cacheKey', now.toIso8601String()); - box.put('lastExecutionTime_Balances', now.toIso8601String()); - - return balance; + // Construire la data : 0xf262a083 + adresse paddée (sans '0x', alignée sur 32 bytes) + final String functionSelector = 'f262a083'; + final String paddedAddress = + address.toLowerCase().replaceFirst('0x', '').padLeft(64, '0'); + final String data = '0x$functionSelector$paddedAddress'; + + final response = await http.post( + Uri.parse('https://rpc.gnosischain.com'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + {"to": contract, "data": data}, + "latest" + ], + "id": 1 + }), + ); + + if (response.statusCode == 200) { + final responseBody = json.decode(response.body); + final result = responseBody['result']; + + debugPrint("🚀 apiService: fetchVaultBalance -> Requête lancée"); + + if (result != null && result != "0x" && result.length >= 66) { + // On suppose que le solde est dans le 1er mot (64 caractères hex après le "0x") + final String balanceHex = result.substring(2, 66); + final balance = BigInt.parse(balanceHex, radix: 16); + + debugPrint( + "✅ apiService: fetchVaultBalance -> Balance récupérée: $balance"); + await box.put(cacheKey, balance.toString()); + await box.put('lastFetchTime_$cacheKey', now.toIso8601String()); + box.put('lastExecutionTime_Balances', now.toIso8601String()); + + return balance; + } else { + debugPrint( + "⚠️ apiService: fetchVaultBalance -> Résultat invalide pour $contract: $result"); + } } else { - debugPrint("⚠️ apiService: fetchVaultBalance -> Résultat invalide pour $contract: $result"); + debugPrint( + '❌ apiService: fetchVaultBalance -> Échec HTTP. Code: ${response.statusCode}'); } - } else { - debugPrint('❌ apiService: fetchVaultBalance -> Échec HTTP. Code: ${response.statusCode}'); - } - return null; -} + return null; + } // Nouvelle méthode pour récupérer les détails des loyers - static Future>> fetchDetailedRentDataForAllWallets({bool forceFetch = false}) async { + static Future>> fetchDetailedRentDataForAllWallets( + {bool forceFetch = false}) async { SharedPreferences prefs = await SharedPreferences.getInstance(); List evmAddresses = prefs.getStringList('evmAddresses') ?? []; - debugPrint("📋 ${evmAddresses.length} wallets à consulter: ${evmAddresses.join(', ')}"); + debugPrint( + "📋 ${evmAddresses.length} wallets à consulter: ${evmAddresses.join(', ')}"); if (evmAddresses.isEmpty) { - debugPrint("⚠️ Aucun wallet renseigné pour les données détaillées de loyer"); + debugPrint( + "⚠️ Aucun wallet renseigné pour les données détaillées de loyer"); return []; } final box = await Hive.openBox('detailedRentData'); final DateTime now = DateTime.now(); - + // Calculer le début de la semaine actuelle (lundi) - final DateTime startOfCurrentWeek = now.subtract(Duration(days: now.weekday - 1)); - final DateTime startOfCurrentWeekMidnight = DateTime(startOfCurrentWeek.year, startOfCurrentWeek.month, startOfCurrentWeek.day); - + final DateTime startOfCurrentWeek = + now.subtract(Duration(days: now.weekday - 1)); + final DateTime startOfCurrentWeekMidnight = DateTime( + startOfCurrentWeek.year, + startOfCurrentWeek.month, + startOfCurrentWeek.day); + // TOUJOURS commencer par charger les données existantes de tous les wallets - debugPrint("📦 Chargement des données détaillées existantes pour tous les wallets"); + debugPrint( + "📦 Chargement des données détaillées existantes pour tous les wallets"); List> allRentData = []; await _loadDetailedRentDataFromCache(box, evmAddresses).then((cachedData) { allRentData.addAll(cachedData); - debugPrint("📦 ${allRentData.length} entrées détaillées chargées depuis le cache"); + debugPrint( + "📦 ${allRentData.length} entrées détaillées chargées depuis le cache"); }); - + // Vérifier si une réponse 429 a été reçue récemment final last429Time = box.get('lastDetailedRent429Time'); if (last429Time != null && !forceFetch) { final DateTime last429 = DateTime.parse(last429Time); if (now.difference(last429) < Duration(minutes: 5)) { - debugPrint('⚠️ 429 reçu récemment pour les données détaillées, utilisation des données existantes'); + debugPrint( + '⚠️ 429 reçu récemment pour les données détaillées, utilisation des données existantes'); return allRentData; } } @@ -1201,9 +1320,10 @@ static Future _fetchVaultBalance(String contract, String address, {bool final lastSuccessTime = box.get(lastSuccessKey); final cacheKey = 'cachedDetailedRentData_$walletAddress'; final cachedData = box.get(cacheKey); - + if (lastSuccessTime == null || cachedData == null) { - debugPrint("❌ Wallet $walletAddress: pas de succès récent ou cache détaillé manquant"); + debugPrint( + "❌ Wallet $walletAddress: pas de succès récent ou cache détaillé manquant"); allWalletsProcessedDetailed = false; break; } else { @@ -1213,7 +1333,7 @@ static Future _fetchVaultBalance(String contract, String address, {bool allWalletsProcessedDetailed = false; break; } - + // Vérifier que le cache n'est pas vide ou corrompu try { final List cacheContent = json.decode(cachedData); @@ -1229,37 +1349,48 @@ static Future _fetchVaultBalance(String contract, String address, {bool } } } - + // Vérifier si la dernière mise à jour réussie des données détaillées est trop ancienne (plus de 7 jours) - final lastSuccessfulDetailedFetch = box.get('lastSuccessfulDetailedRentFetch'); + final lastSuccessfulDetailedFetch = + box.get('lastSuccessfulDetailedRentFetch'); bool isDetailedDataTooOld = false; if (lastSuccessfulDetailedFetch != null) { - final DateTime lastDetailedSuccess = DateTime.parse(lastSuccessfulDetailedFetch); - isDetailedDataTooOld = now.difference(lastDetailedSuccess) > Duration(days: 7); + final DateTime lastDetailedSuccess = + DateTime.parse(lastSuccessfulDetailedFetch); + isDetailedDataTooOld = + now.difference(lastDetailedSuccess) > Duration(days: 7); } else { isDetailedDataTooOld = true; // Pas de fetch réussi enregistré } - + // Si tous les wallets sont traités ET qu'on n'est pas mardi ET pas de forceFetch ET que les données ne sont pas trop anciennes, utiliser le cache final bool isTuesday = now.weekday == DateTime.tuesday; - if (allWalletsProcessedDetailed && !isTuesday && !forceFetch && !isDetailedDataTooOld) { - debugPrint("🛑 Tous les wallets traités cette semaine pour les données détaillées, utilisation des données existantes"); + if (allWalletsProcessedDetailed && + !isTuesday && + !forceFetch && + !isDetailedDataTooOld) { + debugPrint( + "🛑 Tous les wallets traités cette semaine pour les données détaillées, utilisation des données existantes"); return allRentData; } - + if (isDetailedDataTooOld) { - debugPrint("⏰ Données détaillées trop anciennes (>7 jours), forçage de la mise à jour"); + debugPrint( + "⏰ Données détaillées trop anciennes (>7 jours), forçage de la mise à jour"); } - - debugPrint("🔄 Certains wallets non traités pour les données détaillées ou c'est mardi, traitement nécessaire"); + + debugPrint( + "🔄 Certains wallets non traités pour les données détaillées ou c'est mardi, traitement nécessaire"); // Sauvegarder les données existantes comme backup - final Map>> existingDetailedDataByWallet = {}; + final Map>> existingDetailedDataByWallet = + {}; for (String walletAddress in evmAddresses) { final cachedData = box.get('cachedDetailedRentData_$walletAddress'); if (cachedData != null) { try { - final List> walletData = List>.from(json.decode(cachedData)); + final List> walletData = + List>.from(json.decode(cachedData)); existingDetailedDataByWallet[walletAddress] = walletData; } catch (e) { debugPrint('⚠️ Erreur lecture cache pour wallet $walletAddress: $e'); @@ -1279,7 +1410,7 @@ static Future _fetchVaultBalance(String contract, String address, {bool final lastSuccessTime = box.get(lastSuccessKey); final cacheKey = 'cachedDetailedRentData_$walletAddress'; final cachedData = box.get(cacheKey); - + if (lastSuccessTime != null && cachedData != null && !forceFetch) { final DateTime lastSuccess = DateTime.parse(lastSuccessTime); if (lastSuccess.isAfter(startOfCurrentWeekMidnight)) { @@ -1287,47 +1418,54 @@ static Future _fetchVaultBalance(String contract, String address, {bool try { final List cacheContent = json.decode(cachedData); if (cacheContent.isNotEmpty) { - debugPrint("✅ Wallet $walletAddress déjà traité cette semaine avec cache détaillé valide"); + debugPrint( + "✅ Wallet $walletAddress déjà traité cette semaine avec cache détaillé valide"); successfulWallets.add(walletAddress); continue; } else { - debugPrint("⚠️ Wallet $walletAddress: cache détaillé vide, retraitement nécessaire"); + debugPrint( + "⚠️ Wallet $walletAddress: cache détaillé vide, retraitement nécessaire"); } } catch (e) { - debugPrint("⚠️ Wallet $walletAddress: cache détaillé corrompu, retraitement nécessaire - $e"); + debugPrint( + "⚠️ Wallet $walletAddress: cache détaillé corrompu, retraitement nécessaire - $e"); } } } walletsToProcess.add(walletAddress); } - debugPrint("🚀 ${walletsToProcess.length} wallets à traiter pour les données détaillées, ${successfulWallets.length} déjà traités"); + debugPrint( + "🚀 ${walletsToProcess.length} wallets à traiter pour les données détaillées, ${successfulWallets.length} déjà traités"); // Traiter les wallets restants un par un for (var walletAddress in walletsToProcess) { debugPrint("🔄 Traitement détaillé du wallet: $walletAddress"); - + try { - final url = '${Parameters.rentTrackerUrl}/detailed_rent_holder/$walletAddress'; + final url = + '${Parameters.rentTrackerUrl}/detailed_rent_holder/$walletAddress'; debugPrint("🌐 Tentative de requête API détaillée pour $walletAddress"); - final response = await http.get(Uri.parse(url)) + final response = await http + .get(Uri.parse(url)) .timeout(Duration(minutes: 2), onTimeout: () { - throw TimeoutException('Timeout après 2 minutes pour le wallet $walletAddress'); + throw TimeoutException( + 'Timeout après 2 minutes pour le wallet $walletAddress'); }); // Si on reçoit un code 429, conserver les données existantes et arrêter if (response.statusCode == 429) { - debugPrint('⚠️ 429 Too Many Requests pour le wallet $walletAddress - conservation des données existantes'); + debugPrint( + '⚠️ 429 Too Many Requests pour le wallet $walletAddress - conservation des données existantes'); await box.put('lastDetailedRent429Time', now.toIso8601String()); break; } // Si la requête réussit if (response.statusCode == 200) { - final List> rentData = List>.from( - json.decode(response.body) - ); + final List> rentData = + List>.from(json.decode(response.body)); // Retirer les anciennes données de ce wallet du merge allRentData.removeWhere((entry) => entry['wallet'] == walletAddress); @@ -1338,29 +1476,37 @@ static Future _fetchVaultBalance(String contract, String address, {bool } // Sauvegarder dans le cache spécifique du wallet avec vérification - final saveSuccess = await _safeCacheSave(box, 'cachedDetailedRentData_$walletAddress', rentData); + final saveSuccess = await _safeCacheSave( + box, 'cachedDetailedRentData_$walletAddress', rentData); if (saveSuccess) { - await box.put('lastDetailedRentSuccess_$walletAddress', now.toIso8601String()); + await box.put('lastDetailedRentSuccess_$walletAddress', + now.toIso8601String()); } else { - debugPrint('⚠️ Échec sauvegarde cache pour $walletAddress, tentative de repli'); + debugPrint( + '⚠️ Échec sauvegarde cache pour $walletAddress, tentative de repli'); // Tentative de repli sans utiliser _safeCacheSave try { - await box.put('cachedDetailedRentData_$walletAddress', json.encode(rentData)); - await box.put('lastDetailedRentSuccess_$walletAddress', now.toIso8601String()); + await box.put('cachedDetailedRentData_$walletAddress', + json.encode(rentData)); + await box.put('lastDetailedRentSuccess_$walletAddress', + now.toIso8601String()); } catch (e) { debugPrint('❌ Échec total sauvegarde pour $walletAddress: $e'); } } - - debugPrint("✅ Requête détaillée réussie pour $walletAddress, ${rentData.length} entrées obtenues"); + + debugPrint( + "✅ Requête détaillée réussie pour $walletAddress, ${rentData.length} entrées obtenues"); allRentData.addAll(rentData); successfulWallets.add(walletAddress); } else { - debugPrint('❌ Échec requête détaillée pour $walletAddress: ${response.statusCode} - conservation des données existantes'); + debugPrint( + '❌ Échec requête détaillée pour $walletAddress: ${response.statusCode} - conservation des données existantes'); // Les données existantes sont déjà dans allRentData, ne rien faire } } catch (e) { - debugPrint('❌ Erreur requête HTTP détaillée pour $walletAddress: $e - conservation des données existantes'); + debugPrint( + '❌ Erreur requête HTTP détaillée pour $walletAddress: $e - conservation des données existantes'); // Les données existantes sont déjà dans allRentData, ne rien faire } } @@ -1374,20 +1520,23 @@ static Future _fetchVaultBalance(String contract, String address, {bool } } if (entriesSansWallet > 0) { - debugPrint('⚠️ $entriesSansWallet entrées sans wallet assignées à "unknown"'); + debugPrint( + '⚠️ $entriesSansWallet entrées sans wallet assignées à "unknown"'); } await box.put('lastExecutionTime_Rents', now.toIso8601String()); // Sauvegarder le cache global TOUJOURS (même en cas d'erreur partielle) await box.put('cachedDetailedRentDataAll', json.encode(allRentData)); - + // Marquer comme succès complet seulement si tous les wallets ont été traités if (successfulWallets.length == evmAddresses.length) { await box.put('lastSuccessfulDetailedRentFetch', now.toIso8601String()); - debugPrint('✅ Succès complet détaillé: ${allRentData.length} entrées (${successfulWallets.length}/${evmAddresses.length} wallets)'); + debugPrint( + '✅ Succès complet détaillé: ${allRentData.length} entrées (${successfulWallets.length}/${evmAddresses.length} wallets)'); } else { - debugPrint('⚠️ Succès partiel détaillé: ${allRentData.length} entrées (${successfulWallets.length}/${evmAddresses.length} wallets)'); + debugPrint( + '⚠️ Succès partiel détaillé: ${allRentData.length} entrées (${successfulWallets.length}/${evmAddresses.length} wallets)'); } // Comptage des entrées par wallet @@ -1404,12 +1553,14 @@ static Future _fetchVaultBalance(String contract, String address, {bool } /// Charge les données détaillées de loyer depuis le cache pour tous les wallets - static Future>> _loadDetailedRentDataFromCache(Box box, List wallets) async { + static Future>> _loadDetailedRentDataFromCache( + Box box, List wallets) async { // Essayer le cache global d'abord final globalCache = box.get('cachedDetailedRentDataAll'); if (globalCache != null) { try { - final List> data = List>.from(json.decode(globalCache)); + final List> data = + List>.from(json.decode(globalCache)); if (data.isNotEmpty) { return data; } @@ -1428,12 +1579,14 @@ static Future _fetchVaultBalance(String contract, String address, {bool } // Méthode utilitaire pour charger les données du cache (version optimisée async) - static Future _loadFromCacheOptimized(Box box, String walletAddress, List> allRentData) async { + static Future _loadFromCacheOptimized(Box box, String walletAddress, + List> allRentData) async { debugPrint('🔄 Tentative de chargement du cache pour $walletAddress'); final cachedData = box.get('cachedDetailedRentData_$walletAddress'); if (cachedData != null) { try { - final List> rentData = List>.from(json.decode(cachedData)); + final List> rentData = + List>.from(json.decode(cachedData)); // Vérifier et ajouter l'adresse du wallet si nécessaire for (var entry in rentData) { @@ -1443,9 +1596,11 @@ static Future _fetchVaultBalance(String contract, String address, {bool } allRentData.addAll(rentData); - debugPrint("✅ Données de loyer chargées du cache pour $walletAddress (${rentData.length} entrées)"); + debugPrint( + "✅ Données de loyer chargées du cache pour $walletAddress (${rentData.length} entrées)"); } catch (e) { - debugPrint('❌ Erreur lors du chargement des données en cache pour $walletAddress: $e'); + debugPrint( + '❌ Erreur lors du chargement des données en cache pour $walletAddress: $e'); } } else { debugPrint('⚠️ Pas de données en cache pour le wallet $walletAddress'); @@ -1457,7 +1612,7 @@ static Future _fetchVaultBalance(String contract, String address, {bool try { final String jsonData = json.encode(data); await box.put(key, jsonData); - + // Vérifier que les données ont été sauvegardées correctement final savedData = box.get(key); if (savedData == jsonData) { @@ -1474,90 +1629,100 @@ static Future _fetchVaultBalance(String contract, String address, {bool } /// Méthode sécurisée pour charger des données depuis le cache avec vérification - static Future>> _safeLoadWalletCache(Box box, String walletAddress) async { + static Future>> _safeLoadWalletCache( + Box box, String walletAddress) async { try { - final cachedData = box.get('cachedDetailedRentData_$walletAddress') ?? - box.get('cachedRentData_$walletAddress'); - + final cachedData = box.get('cachedDetailedRentData_$walletAddress') ?? + box.get('cachedRentData_$walletAddress'); + if (cachedData != null) { - final List> data = List>.from( - json.decode(cachedData) - ); - + final List> data = + List>.from(json.decode(cachedData)); + // Vérifier l'intégrité des données for (var entry in data) { if (!entry.containsKey('wallet') || entry['wallet'] == null) { entry['wallet'] = walletAddress; } } - - debugPrint("✅ Cache chargé avec succès pour $walletAddress (${data.length} entrées)"); + + debugPrint( + "✅ Cache chargé avec succès pour $walletAddress (${data.length} entrées)"); return data; } } catch (e) { debugPrint('❌ Erreur chargement cache pour $walletAddress: $e'); } - + debugPrint('⚠️ Pas de cache valide pour le wallet $walletAddress'); return []; } /// Fonction de diagnostic pour examiner l'état du cache des wallets - static Future> diagnoseCacheStatus(List walletAddresses) async { + static Future> diagnoseCacheStatus( + List walletAddresses) async { final rentBox = Hive.box('realTokens'); final detailedBox = await Hive.openBox('detailedRentData'); - + Map diagnostics = { 'timestamp': DateTime.now().toIso8601String(), 'walletDiagnostics': {}, 'globalCacheStatus': {}, }; - + // Vérifier le cache global diagnostics['globalCacheStatus'] = { 'cachedRentData': rentBox.get('cachedRentData') != null, - 'cachedDetailedRentDataAll': detailedBox.get('cachedDetailedRentDataAll') != null, + 'cachedDetailedRentDataAll': + detailedBox.get('cachedDetailedRentDataAll') != null, 'lastRentFetchTime': rentBox.get('lastRentFetchTime'), 'lastSuccessfulRentFetch': rentBox.get('lastSuccessfulRentFetch'), - 'lastSuccessfulDetailedRentFetch': detailedBox.get('lastSuccessfulDetailedRentFetch'), + 'lastSuccessfulDetailedRentFetch': + detailedBox.get('lastSuccessfulDetailedRentFetch'), 'lastRent429Time': rentBox.get('lastRent429Time'), 'lastDetailedRent429Time': detailedBox.get('lastDetailedRent429Time'), }; - + // Vérifier chaque wallet individuellement for (String walletAddress in walletAddresses) { try { - final rentCacheExists = rentBox.get('cachedRentData_$walletAddress') != null; - final detailedCacheExists = detailedBox.get('cachedDetailedRentData_$walletAddress') != null; - + final rentCacheExists = + rentBox.get('cachedRentData_$walletAddress') != null; + final detailedCacheExists = + detailedBox.get('cachedDetailedRentData_$walletAddress') != null; + int rentCacheEntries = 0; int detailedCacheEntries = 0; - + if (rentCacheExists) { try { final rentData = await _safeLoadWalletCache(rentBox, walletAddress); rentCacheEntries = rentData.length; } catch (e) { - debugPrint('❌ Erreur lecture cache rent pour diagnostic $walletAddress: $e'); + debugPrint( + '❌ Erreur lecture cache rent pour diagnostic $walletAddress: $e'); } } - + if (detailedCacheExists) { try { - final detailedData = await _safeLoadWalletCache(detailedBox, walletAddress); + final detailedData = + await _safeLoadWalletCache(detailedBox, walletAddress); detailedCacheEntries = detailedData.length; } catch (e) { - debugPrint('❌ Erreur lecture cache detailed pour diagnostic $walletAddress: $e'); + debugPrint( + '❌ Erreur lecture cache detailed pour diagnostic $walletAddress: $e'); } } - + diagnostics['walletDiagnostics'][walletAddress] = { 'rentCacheExists': rentCacheExists, 'detailedCacheExists': detailedCacheExists, 'rentCacheEntries': rentCacheEntries, 'detailedCacheEntries': detailedCacheEntries, 'lastRentSuccess': rentBox.get('lastRentSuccess_$walletAddress'), - 'lastDetailedRentSuccess': detailedBox.get('lastDetailedRentSuccess_$walletAddress'), + 'lastDetailedRentSuccess': + detailedBox.get('lastDetailedRentSuccess_$walletAddress'), }; } catch (e) { diagnostics['walletDiagnostics'][walletAddress] = { @@ -1565,70 +1730,79 @@ static Future _fetchVaultBalance(String contract, String address, {bool }; } } - - debugPrint('📊 Diagnostic cache terminé pour ${walletAddresses.length} wallets'); + + debugPrint( + '📊 Diagnostic cache terminé pour ${walletAddresses.length} wallets'); return diagnostics; } // Nouvelle méthode pour récupérer les propriétés en cours de vente - static Future>> fetchPropertiesForSale({bool forceFetch = false}) async { + static Future>> fetchPropertiesForSale( + {bool forceFetch = false}) async { return _fetchWithCache>>( cacheKey: 'cachedPropertiesForSale', debugName: "Properties For Sale", forceFetch: forceFetch, - customCacheDuration: Duration(hours: 6), // Cache de 6 heures pour les propriétés en vente + customCacheDuration: + Duration(hours: 6), // Cache de 6 heures pour les propriétés en vente fromJson: (data) => List>.from(data), toJson: (data) => data, emptyValue: >[], apiCall: () async { const url = 'https://realt.co/wp-json/realt/v1/products/for_sale'; - + debugPrint("🔄 Récupération des propriétés en vente"); - final response = await http.get(Uri.parse(url)) - .timeout(Duration(seconds: 30)); + final response = + await http.get(Uri.parse(url)).timeout(Duration(seconds: 30)); if (response.statusCode == 200) { // Décoder la réponse JSON final data = json.decode(response.body); - + // Extraire la liste de produits - final List> properties = List>.from(data['products']); - + final List> properties = + List>.from(data['products']); + debugPrint("✅ ${properties.length} propriétés en vente récupérées"); return properties; } else { - throw Exception('Échec de la requête propriétés. Code: ${response.statusCode}'); + throw Exception( + 'Échec de la requête propriétés. Code: ${response.statusCode}'); } }, ); } - static Future> fetchTokenVolumes({bool forceFetch = false}) async { + static Future> fetchTokenVolumes( + {bool forceFetch = false}) async { return _fetchWithCacheList( cacheKey: 'cachedTokenVolumesData', debugName: "Token Volumes", forceFetch: forceFetch, - customCacheDuration: Duration(hours: 4), // Cache de 4 heures pour les volumes + customCacheDuration: + Duration(hours: 4), // Cache de 4 heures pour les volumes apiCall: () async { final apiUrl = '${Parameters.mainApiUrl}/tokens_volume/'; debugPrint("🔄 Récupération des volumes de tokens"); - - final response = await http.get(Uri.parse(apiUrl)) - .timeout(Duration(seconds: 30)); + + final response = + await http.get(Uri.parse(apiUrl)).timeout(Duration(seconds: 30)); if (response.statusCode == 200) { final data = json.decode(response.body); debugPrint("✅ Volumes de tokens récupérés"); return data; } else { - throw Exception("Échec de la récupération depuis FastAPI: ${response.statusCode}"); + throw Exception( + "Échec de la récupération depuis FastAPI: ${response.statusCode}"); } }, ); } - static Future> fetchTransactionsHistory({bool forceFetch = false}) async { + static Future> fetchTransactionsHistory( + {bool forceFetch = false}) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); List evmAddresses = prefs.getStringList('evmAddresses') ?? []; @@ -1640,16 +1814,19 @@ static Future _fetchVaultBalance(String contract, String address, {bool cacheKey: 'cachedTransactionsData_transactions_history', debugName: "Transactions History", forceFetch: forceFetch, - customCacheDuration: Duration(hours: 3), // Cache de 3 heures pour l'historique + customCacheDuration: + Duration(hours: 3), // Cache de 3 heures pour l'historique apiCall: () async { // Utiliser le traitement parallèle pour l'historique des transactions - final allTransactionResults = await _processWalletsInParallel>( + final allTransactionResults = + await _processWalletsInParallel>( wallets: evmAddresses, debugName: "historique transactions", maxConcurrentRequests: 3, // Plus de concurrence pour l'historique processWallet: (wallet) async { - final apiUrl = '${Parameters.mainApiUrl}/transactions_history/$wallet'; - + final apiUrl = + '${Parameters.mainApiUrl}/transactions_history/$wallet'; + final response = await _httpGetWithRetry( apiUrl, timeout: _mediumTimeout, @@ -1661,7 +1838,8 @@ static Future _fetchVaultBalance(String contract, String address, {bool debugPrint("✅ Transactions récupérées pour wallet: $wallet"); return walletData; } else { - debugPrint("⚠️ Erreur récupération transactions pour wallet: $wallet (HTTP ${response.statusCode})"); + debugPrint( + "⚠️ Erreur récupération transactions pour wallet: $wallet (HTTP ${response.statusCode})"); return null; } }, @@ -1673,13 +1851,15 @@ static Future _fetchVaultBalance(String contract, String address, {bool allTransactions.addAll(transactionList); } - debugPrint("✅ ${allTransactions.length} transactions récupérées au total"); + debugPrint( + "✅ ${allTransactions.length} transactions récupérées au total"); return allTransactions; }, ); } - static Future> fetchYamWalletsTransactions({bool forceFetch = false}) async { + static Future> fetchYamWalletsTransactions( + {bool forceFetch = false}) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); List evmAddresses = prefs.getStringList('evmAddresses') ?? []; @@ -1691,16 +1871,19 @@ static Future _fetchVaultBalance(String contract, String address, {bool cacheKey: 'cachedTransactionsData_yam_wallet_transactions', debugName: "YAM Wallets Transactions", forceFetch: forceFetch, - customCacheDuration: Duration(hours: 3), // Cache de 3 heures pour les transactions YAM + customCacheDuration: + Duration(hours: 3), // Cache de 3 heures pour les transactions YAM apiCall: () async { // Utiliser le traitement parallèle pour les transactions YAM - final allYamTransactionResults = await _processWalletsInParallel>( + final allYamTransactionResults = + await _processWalletsInParallel>( wallets: evmAddresses, debugName: "transactions YAM", maxConcurrentRequests: 3, processWallet: (wallet) async { - final apiUrl = '${Parameters.mainApiUrl}/YAM_transactions_history/$wallet'; - + final apiUrl = + '${Parameters.mainApiUrl}/YAM_transactions_history/$wallet'; + final response = await _httpGetWithRetry( apiUrl, timeout: _mediumTimeout, @@ -1712,7 +1895,8 @@ static Future _fetchVaultBalance(String contract, String address, {bool debugPrint("✅ Transactions YAM récupérées pour wallet: $wallet"); return walletData; } else { - debugPrint("⚠️ Erreur récupération transactions YAM pour wallet: $wallet (HTTP ${response.statusCode})"); + debugPrint( + "⚠️ Erreur récupération transactions YAM pour wallet: $wallet (HTTP ${response.statusCode})"); return null; } }, @@ -1724,30 +1908,37 @@ static Future _fetchVaultBalance(String contract, String address, {bool allYamTransactions.addAll(transactionList); } - debugPrint("✅ ${allYamTransactions.length} transactions YAM récupérées au total"); + debugPrint( + "✅ ${allYamTransactions.length} transactions YAM récupérées au total"); return allYamTransactions; }, ); } - static Future>> fetchRmmBalancesForAddress(String address, {bool forceFetch = false}) async { + static Future>> fetchRmmBalancesForAddress( + String address, + {bool forceFetch = false}) async { return _fetchWithCache>>( cacheKey: 'cachedRmmBalancesForAddress_$address', debugName: "RMM Balances for $address", forceFetch: forceFetch, - customCacheDuration: Duration(minutes: 15), // Cache court pour les balances individuelles + customCacheDuration: + Duration(minutes: 15), // Cache court pour les balances individuelles fromJson: (data) => List>.from(data), toJson: (data) => data, emptyValue: >[], apiCall: () async { // Contrats pour USDC & XDAI (dépôt et emprunt) - const String usdcDepositContract = ContractsConstants.usdcDepositContract; + const String usdcDepositContract = + ContractsConstants.usdcDepositContract; const String usdcBorrowContract = ContractsConstants.usdcBorrowContract; - const String xdaiDepositContract = ContractsConstants.xdaiDepositContract; + const String xdaiDepositContract = + ContractsConstants.xdaiDepositContract; const String xdaiBorrowContract = ContractsConstants.xdaiBorrowContract; const String gnosisUsdcContract = ContractsConstants.gnosisUsdcContract; const String gnosisRegContract = ContractsConstants.gnosisRegContract; - const String gnosisVaultRegContract = ContractsConstants.gnosisVaultRegContract; + const String gnosisVaultRegContract = + ContractsConstants.gnosisVaultRegContract; debugPrint("🔄 Récupération des balances RMM pour l'adresse: $address"); @@ -1759,7 +1950,8 @@ static Future _fetchVaultBalance(String contract, String address, {bool _fetchBalance(xdaiBorrowContract, address, forceFetch: forceFetch), _fetchBalance(gnosisUsdcContract, address, forceFetch: forceFetch), _fetchBalance(gnosisRegContract, address, forceFetch: forceFetch), - _fetchVaultBalance(gnosisVaultRegContract, address, forceFetch: forceFetch), + _fetchVaultBalance(gnosisVaultRegContract, address, + forceFetch: forceFetch), _fetchNativeBalance(address, forceFetch: forceFetch), ]); @@ -1774,22 +1966,32 @@ static Future _fetchVaultBalance(String contract, String address, {bool gnosisXdaiResponse, ] = futures; - if (usdcDepositResponse != null && usdcBorrowResponse != null && - xdaiDepositResponse != null && xdaiBorrowResponse != null && - gnosisUsdcResponse != null && gnosisXdaiResponse != null) { - + if (usdcDepositResponse != null && + usdcBorrowResponse != null && + xdaiDepositResponse != null && + xdaiBorrowResponse != null && + gnosisUsdcResponse != null && + gnosisXdaiResponse != null) { final timestamp = DateTime.now().toIso8601String(); - double usdcDepositBalance = PerformanceUtils.bigIntToDouble(usdcDepositResponse, 6); - double usdcBorrowBalance = PerformanceUtils.bigIntToDouble(usdcBorrowResponse, 6); - double xdaiDepositBalance = PerformanceUtils.bigIntToDouble(xdaiDepositResponse, 18); - double xdaiBorrowBalance = PerformanceUtils.bigIntToDouble(xdaiBorrowResponse, 18); - double gnosisUsdcBalance = PerformanceUtils.bigIntToDouble(gnosisUsdcResponse, 6); - double gnosisRegBalance = PerformanceUtils.bigIntToDouble(gnosisRegResponse ?? BigInt.zero, 18); - double gnosisVaultRegBalance = PerformanceUtils.bigIntToDouble(gnosisVaultRegResponse ?? BigInt.zero, 18); - double gnosisXdaiBalance = PerformanceUtils.bigIntToDouble(gnosisXdaiResponse, 18); - + double usdcDepositBalance = + PerformanceUtils.bigIntToDouble(usdcDepositResponse, 6); + double usdcBorrowBalance = + PerformanceUtils.bigIntToDouble(usdcBorrowResponse, 6); + double xdaiDepositBalance = + PerformanceUtils.bigIntToDouble(xdaiDepositResponse, 18); + double xdaiBorrowBalance = + PerformanceUtils.bigIntToDouble(xdaiBorrowResponse, 18); + double gnosisUsdcBalance = + PerformanceUtils.bigIntToDouble(gnosisUsdcResponse, 6); + double gnosisRegBalance = PerformanceUtils.bigIntToDouble( + gnosisRegResponse ?? BigInt.zero, 18); + double gnosisVaultRegBalance = PerformanceUtils.bigIntToDouble( + gnosisVaultRegResponse ?? BigInt.zero, 18); + double gnosisXdaiBalance = + PerformanceUtils.bigIntToDouble(gnosisXdaiResponse, 18); + debugPrint("✅ Balances RMM récupérées pour l'adresse: $address"); - + return [ { 'address': address, @@ -1812,16 +2014,18 @@ static Future _fetchVaultBalance(String contract, String address, {bool } /// Récupère l'historique des tokens depuis l'API token_history - static Future> fetchTokenHistory({bool forceFetch = false}) async { + static Future> fetchTokenHistory( + {bool forceFetch = false}) async { return _fetchWithCacheList( cacheKey: 'cachedTokenHistoryData', debugName: "Token History", forceFetch: forceFetch, - customCacheDuration: Duration(hours: 6), // Cache de 6 heures pour l'historique + customCacheDuration: + Duration(hours: 6), // Cache de 6 heures pour l'historique apiCall: () async { const apiUrl = 'https://api.vfhome.fr/token_history/?limit=10000'; debugPrint("🔄 Récupération de l'historique des tokens"); - + final response = await _httpGetWithRetry( apiUrl, timeout: _longTimeout, @@ -1831,15 +2035,19 @@ static Future _fetchVaultBalance(String contract, String address, {bool if (response.statusCode == 200) { final data = json.decode(response.body); if (data is List) { - debugPrint("✅ Historique des tokens récupéré: ${data.length} entrées"); + debugPrint( + "✅ Historique des tokens récupéré: ${data.length} entrées"); return data; } else { - debugPrint("⚠️ Format de données inattendu pour l'historique des tokens"); + debugPrint( + "⚠️ Format de données inattendu pour l'historique des tokens"); return []; } } else { - debugPrint("❌ Erreur récupération historique tokens: HTTP ${response.statusCode}"); - throw Exception("Échec de la récupération de l'historique: ${response.statusCode}"); + debugPrint( + "❌ Erreur récupération historique tokens: HTTP ${response.statusCode}"); + throw Exception( + "Échec de la récupération de l'historique: ${response.statusCode}"); } }, ); diff --git a/lib/services/api_service_helpers.dart b/lib/services/api_service_helpers.dart index 0693b02..7915134 100644 --- a/lib/services/api_service_helpers.dart +++ b/lib/services/api_service_helpers.dart @@ -6,7 +6,6 @@ import 'package:shared_preferences/shared_preferences.dart'; /// Helpers pour factoriser les patterns répétitifs dans ApiService class ApiServiceHelpers { - /// Pattern générique pour fetch des données depuis plusieurs wallets /// Réduit la duplication de code dans fetchWalletTokens, fetchTransactionsHistory, etc. static Future> fetchFromMultipleWallets({ @@ -16,7 +15,8 @@ class ApiServiceHelpers { List? customWallets, }) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - final List wallets = customWallets ?? prefs.getStringList('evmAddresses') ?? []; + final List wallets = + customWallets ?? prefs.getStringList('evmAddresses') ?? []; if (wallets.isEmpty) { debugPrint("⚠️ Aucun wallet renseigné pour $debugName"); @@ -31,11 +31,12 @@ class ApiServiceHelpers { for (String wallet in wallets) { final apiUrl = urlBuilder(wallet); - + try { - final response = await http.get(Uri.parse(apiUrl)) - .timeout(timeout, onTimeout: () { - throw TimeoutException('Délai dépassé pour $debugName du wallet $wallet'); + final response = + await http.get(Uri.parse(apiUrl)).timeout(timeout, onTimeout: () { + throw TimeoutException( + 'Délai dépassé pour $debugName du wallet $wallet'); }); if (response.statusCode == 200) { @@ -43,13 +44,15 @@ class ApiServiceHelpers { if (walletData is List && walletData.isNotEmpty) { allData.addAll(walletData); successCount++; - debugPrint("✅ $debugName récupéré pour wallet: $wallet (${walletData.length} éléments)"); + debugPrint( + "✅ $debugName récupéré pour wallet: $wallet (${walletData.length} éléments)"); } else { debugPrint("⚠️ Aucune donnée $debugName pour wallet: $wallet"); } } else { errorCount++; - debugPrint("❌ Erreur $debugName pour wallet $wallet: HTTP ${response.statusCode}"); + debugPrint( + "❌ Erreur $debugName pour wallet $wallet: HTTP ${response.statusCode}"); } } catch (e) { errorCount++; @@ -57,7 +60,8 @@ class ApiServiceHelpers { } } - debugPrint("📊 Récapitulatif $debugName: $successCount wallets réussis, $errorCount en erreur"); + debugPrint( + "📊 Récapitulatif $debugName: $successCount wallets réussis, $errorCount en erreur"); debugPrint("✅ ${allData.length} éléments $debugName récupérés au total"); return allData; @@ -69,12 +73,15 @@ class ApiServiceHelpers { required String debugName, required String Function(String wallet) urlBuilder, required Duration timeout, - required Function(String wallet, List> allData) onCacheLoad, - required Function(String wallet, List> data) onDataProcess, + required Function(String wallet, List> allData) + onCacheLoad, + required Function(String wallet, List> data) + onDataProcess, List? customWallets, }) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - final List wallets = customWallets ?? prefs.getStringList('evmAddresses') ?? []; + final List wallets = + customWallets ?? prefs.getStringList('evmAddresses') ?? []; if (wallets.isEmpty) { debugPrint("⚠️ Aucun wallet renseigné pour $debugName"); @@ -88,33 +95,35 @@ class ApiServiceHelpers { for (String wallet in wallets) { final url = urlBuilder(wallet); - + try { debugPrint("🌐 Tentative de requête $debugName pour $wallet"); - final response = await http.get(Uri.parse(url)) - .timeout(timeout, onTimeout: () { + final response = + await http.get(Uri.parse(url)).timeout(timeout, onTimeout: () { throw TimeoutException('Timeout pour $debugName du wallet $wallet'); }); // Gestion spéciale des erreurs 429 if (response.statusCode == 429) { - debugPrint('⚠️ 429 Too Many Requests pour $debugName du wallet $wallet'); + debugPrint( + '⚠️ 429 Too Many Requests pour $debugName du wallet $wallet'); onCacheLoad(wallet, allData); hasError = true; break; } if (response.statusCode == 200) { - final List> walletData = List>.from( - json.decode(response.body) - ); - + final List> walletData = + List>.from(json.decode(response.body)); + onDataProcess(wallet, walletData); allData.addAll(walletData); - debugPrint("✅ $debugName récupéré pour $wallet: ${walletData.length} entrées"); + debugPrint( + "✅ $debugName récupéré pour $wallet: ${walletData.length} entrées"); } else { - debugPrint('❌ Erreur $debugName pour $wallet: HTTP ${response.statusCode}'); + debugPrint( + '❌ Erreur $debugName pour $wallet: HTTP ${response.statusCode}'); onCacheLoad(wallet, allData); hasError = true; } @@ -126,7 +135,8 @@ class ApiServiceHelpers { } if (hasError) { - throw Exception("Erreurs rencontrées lors de la récupération de $debugName"); + throw Exception( + "Erreurs rencontrées lors de la récupération de $debugName"); } debugPrint('✅ $debugName terminé - ${allData.length} entrées au total'); @@ -157,7 +167,9 @@ class ApiServiceHelpers { debugPrint("❌ Erreur $operation pour $wallet: $error"); } - static void logApiSummary(String operation, int successCount, int errorCount, int totalItems) { - debugPrint("📊 Récapitulatif $operation: $successCount réussis, $errorCount erreurs, $totalItems éléments total"); + static void logApiSummary( + String operation, int successCount, int errorCount, int totalItems) { + debugPrint( + "📊 Récapitulatif $operation: $successCount réussis, $errorCount erreurs, $totalItems éléments total"); } -} \ No newline at end of file +} diff --git a/lib/services/biometric_service.dart b/lib/services/biometric_service.dart index d305dcc..9874f2e 100644 --- a/lib/services/biometric_service.dart +++ b/lib/services/biometric_service.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:local_auth/error_codes.dart' as auth_error; import 'package:shared_preferences/shared_preferences.dart'; class BiometricService { @@ -58,7 +57,8 @@ class BiometricService { try { return await _localAuth.getAvailableBiometrics(); } catch (e) { - debugPrint('Erreur lors de la récupération des biométries disponibles: $e'); + debugPrint( + 'Erreur lors de la récupération des biométries disponibles: $e'); return []; } } @@ -79,7 +79,8 @@ class BiometricService { } } - Future authenticate({String reason = 'Veuillez vous authentifier pour continuer'}) async { + Future authenticate( + {String reason = 'Veuillez vous authentifier pour continuer'}) async { // Si la biométrie n'est pas disponible ou pas activée, considérer comme authentifié if (!await isBiometricAvailable() || !await isBiometricEnabled()) { return true; @@ -110,7 +111,8 @@ class BiometricService { options: const AuthenticationOptions( stickyAuth: true, biometricOnly: true, - sensitiveTransaction: true, // Indique que c'est une transaction sensible + sensitiveTransaction: + true, // Indique que c'est une transaction sensible useErrorDialogs: true, // Utiliser les dialogues d'erreur du système ), ); diff --git a/lib/services/cache_service.dart b/lib/services/cache_service.dart index 341206b..fd9e9a0 100644 --- a/lib/services/cache_service.dart +++ b/lib/services/cache_service.dart @@ -26,17 +26,17 @@ class CacheService { }) async { final DateTime now = DateTime.now(); final lastFetchTime = _box.get('lastFetchTime_$cacheKey'); - + // 1. Toujours charger le cache en premier si disponible List> cachedData = []; try { String? cachedJson = _box.get(cacheKey); - + // Essayer la clé alternative si la principale est vide if (cachedJson == null && alternativeCacheKey != null) { cachedJson = _box.get(alternativeCacheKey); } - + if (cachedJson != null) { cachedData = List>.from(json.decode(cachedJson)); debugPrint("🔵 Cache $debugName chargé: ${cachedData.length} éléments"); @@ -64,7 +64,7 @@ class CacheService { try { debugPrint("🔄 Mise à jour API $debugName..."); final apiResult = await apiCall(); - + if (apiResult.isNotEmpty) { // Sauvegarder le nouveau cache final newData = List>.from(apiResult); @@ -101,20 +101,21 @@ class CacheService { }) async { final DateTime now = DateTime.now(); final lastFetchTime = _box.get('lastFetchTime_$cacheKey'); - + // 1. Charger et notifier immédiatement avec le cache si disponible List> cachedData = []; try { String? cachedJson = _box.get(cacheKey); - + if (cachedJson == null && alternativeCacheKey != null) { cachedJson = _box.get(alternativeCacheKey); } - + if (cachedJson != null) { cachedData = List>.from(json.decode(cachedJson)); onDataUpdated(cachedData); // Notification immédiate pour l'UI - debugPrint("🔵 Cache $debugName restauré et notifié: ${cachedData.length} éléments"); + debugPrint( + "🔵 Cache $debugName restauré et notifié: ${cachedData.length} éléments"); } } catch (e) { debugPrint("⚠️ Erreur chargement cache $debugName: $e"); @@ -138,16 +139,17 @@ class CacheService { try { debugPrint("🔄 Mise à jour API $debugName en arrière-plan..."); final apiResult = await apiCall(); - + if (apiResult.isNotEmpty) { final newData = List>.from(apiResult); - + // Vérifier si les données ont changé avant de notifier if (!_areDataListsEqual(cachedData, newData)) { await _box.put(cacheKey, json.encode(newData)); await _box.put('lastFetchTime_$cacheKey', now.toIso8601String()); onDataUpdated(newData); // Notification avec les nouvelles données - debugPrint("💾 $debugName mis à jour et notifié: ${newData.length} éléments"); + debugPrint( + "💾 $debugName mis à jour et notifié: ${newData.length} éléments"); } else { // Mettre à jour le timestamp même si les données sont identiques await _box.put('lastFetchTime_$cacheKey', now.toIso8601String()); @@ -158,15 +160,17 @@ class CacheService { } } catch (e) { debugPrint("❌ Erreur API $debugName: $e"); - + // En cas d'erreur, s'assurer que les données du cache sont notifiées if (cachedData.isEmpty) { try { String? cachedJson = _box.get(cacheKey); if (cachedJson != null) { - var fallbackData = List>.from(json.decode(cachedJson)); + var fallbackData = + List>.from(json.decode(cachedJson)); onDataUpdated(fallbackData); - debugPrint("🔄 Fallback cache $debugName notifié: ${fallbackData.length} éléments"); + debugPrint( + "🔄 Fallback cache $debugName notifié: ${fallbackData.length} éléments"); } } catch (cacheError) { debugPrint("❌ Erreur fallback cache $debugName: $cacheError"); @@ -176,16 +180,17 @@ class CacheService { } /// Compare deux listes de données pour détecter les changements - bool _areDataListsEqual(List> list1, List> list2) { + bool _areDataListsEqual( + List> list1, List> list2) { if (list1.length != list2.length) return false; if (list1.isEmpty && list2.isEmpty) return true; - + try { // Comparaison rapide basée sur la taille et les premiers/derniers éléments if (list1.length != list2.length) return false; if (list1.isNotEmpty && list2.isNotEmpty) { return json.encode(list1.first) == json.encode(list2.first) && - json.encode(list1.last) == json.encode(list2.last); + json.encode(list1.last) == json.encode(list2.last); } return true; } catch (e) { @@ -195,14 +200,15 @@ class CacheService { } /// Méthode pour obtenir des données du cache uniquement - List> getCachedData(String cacheKey, {String? alternativeCacheKey}) { + List> getCachedData(String cacheKey, + {String? alternativeCacheKey}) { try { String? cachedJson = _box.get(cacheKey); - + if (cachedJson == null && alternativeCacheKey != null) { cachedJson = _box.get(alternativeCacheKey); } - + if (cachedJson != null) { return List>.from(json.decode(cachedJson)); } @@ -216,7 +222,7 @@ class CacheService { bool isCacheValid(String cacheKey) { final lastFetchTime = _box.get('lastFetchTime_$cacheKey'); if (lastFetchTime == null) return false; - + try { final DateTime lastFetch = DateTime.parse(lastFetchTime); return DateTime.now().difference(lastFetch) < Parameters.apiCacheDuration; @@ -236,4 +242,4 @@ class CacheService { await _box.clear(); debugPrint("🗑️ Tout le cache effacé"); } -} \ No newline at end of file +} diff --git a/lib/services/google_drive_service.dart b/lib/services/google_drive_service.dart index 8e0f001..ed5b07b 100644 --- a/lib/services/google_drive_service.dart +++ b/lib/services/google_drive_service.dart @@ -24,13 +24,16 @@ class GoogleDriveService { /// Vérifier la connexion et initialiser l'API Google Drive Future initDrive() async { - final GoogleSignInAccount? googleUser = await _googleSignIn.signInSilently(); + final GoogleSignInAccount? googleUser = + await _googleSignIn.signInSilently(); if (googleUser != null) { - final GoogleSignInAuthentication googleAuth = await googleUser.authentication; + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; final auth.AuthClient client = auth.authenticatedClient( http.Client(), auth.AccessCredentials( - auth.AccessToken('Bearer', googleAuth.accessToken!, DateTime.now().add(Duration(hours: 1)).toUtc()), + auth.AccessToken('Bearer', googleAuth.accessToken!, + DateTime.now().add(Duration(hours: 1)).toUtc()), googleAuth.idToken, _googleSignIn.scopes, ), @@ -107,8 +110,10 @@ class GoogleDriveService { // Si les données locales sont vides, on ne fait que l'importation sans upload if (localData.isEmpty && driveData != null) { - debugPrint("📥 Importation des données de Google Drive dans l'application..."); - await _restoreLocalBackup("${(await getApplicationDocumentsDirectory()).path}/realToken_Backup.zip"); + debugPrint( + "📥 Importation des données de Google Drive dans l'application..."); + await _restoreLocalBackup( + "${(await getApplicationDocumentsDirectory()).path}/realToken_Backup.zip"); return; } @@ -116,7 +121,8 @@ class GoogleDriveService { Map mergedData = _mergeData(localData, driveData); // 🔹 Assurer que les données sont bien enregistrées dans Hive - debugPrint("📌 Contenu fusionné après merge (avant stockage dans Hive) : ${jsonEncode(mergedData)}"); + debugPrint( + "📌 Contenu fusionné après merge (avant stockage dans Hive) : ${jsonEncode(mergedData)}"); await _storeMergedDataInHive(mergedData); DataFetchUtils.refreshData(context); @@ -147,8 +153,10 @@ class GoogleDriveService { if (mergedData.containsKey(key)) { // debugPrint("🔍 Clés à stocker dans '$key' : ${mergedData[key].keys}"); - Map existingData = Map.from(box.toMap()); - Map newData = Map.from(mergedData[key]); + Map existingData = + Map.from(box.toMap()); + Map newData = + Map.from(mergedData[key]); newData.forEach((dataKey, value) { // ✅ Si la clé n'existe pas, on l'ajoute directement @@ -169,10 +177,14 @@ class GoogleDriveService { String newTimestamp = newItem['timestamp']; // Vérifier si un élément avec le même timestamp existe déjà - bool exists = updatedList.any((existingItem) => existingItem is Map && existingItem.containsKey('timestamp') && existingItem['timestamp'] == newTimestamp); + bool exists = updatedList.any((existingItem) => + existingItem is Map && + existingItem.containsKey('timestamp') && + existingItem['timestamp'] == newTimestamp); if (!exists) { - debugPrint("➕ Ajouté (nouveau timestamp) dans '$key' : $newItem"); + debugPrint( + "➕ Ajouté (nouveau timestamp) dans '$key' : $newItem"); updatedList.add(newItem); } } @@ -182,14 +194,18 @@ class GoogleDriveService { } else if (value is Map) { // Gérer les objets Map individuellement value.forEach((subKey, subValue) { - if (!existingValue.containsKey(subKey) || (existingValue[subKey]['timestamp'] ?? '') < (subValue['timestamp'] ?? '')) { - debugPrint("🔄 Mise à jour (timestamp plus récent) dans '$key' : $subKey -> $subValue"); + if (!existingValue.containsKey(subKey) || + (existingValue[subKey]['timestamp'] ?? '') < + (subValue['timestamp'] ?? '')) { + debugPrint( + "🔄 Mise à jour (timestamp plus récent) dans '$key' : $subKey -> $subValue"); existingValue[subKey] = subValue; } }); box.put(dataKey, existingValue); } else { - debugPrint("⚠️ Valeur ignorée (format non pris en charge) : $dataKey -> $value"); + debugPrint( + "⚠️ Valeur ignorée (format non pris en charge) : $dataKey -> $value"); } } }); @@ -258,7 +274,8 @@ class GoogleDriveService { } } - Future?> _extractAndCleanBackupData(String zipFilePath) async { + Future?> _extractAndCleanBackupData( + String zipFilePath) async { final directory = await getApplicationDocumentsDirectory(); final File zipFile = File(zipFilePath); @@ -288,7 +305,8 @@ class GoogleDriveService { } } - debugPrint("✅ Extraction et nettoyage terminés, données récupérées "); //: $extractedData + debugPrint( + "✅ Extraction et nettoyage terminés, données récupérées "); //: $extractedData return extractedData; } @@ -390,7 +408,8 @@ class GoogleDriveService { if (drivePreferences.isNotEmpty) { _storeMergedPreferences(drivePreferences); } else { - debugPrint("⚠️ Google Drive ne contient pas de préférences, on garde celles en local."); + debugPrint( + "⚠️ Google Drive ne contient pas de préférences, on garde celles en local."); } } } @@ -437,37 +456,46 @@ class GoogleDriveService { return value; } - Future _storeMergedPreferences(Map mergedPreferences) async { - debugPrint("📦 Sauvegarde des préférences fusionnées dans SharedPreferences..."); + Future _storeMergedPreferences( + Map mergedPreferences) async { + debugPrint( + "📦 Sauvegarde des préférences fusionnées dans SharedPreferences..."); final prefs = await SharedPreferences.getInstance(); // 🔹 Stocker les adresses ETH - await prefs.setStringList('evmAddresses', List.from(mergedPreferences['ethAddresses'] ?? [])); + await prefs.setStringList('evmAddresses', + List.from(mergedPreferences['ethAddresses'] ?? [])); // 🔹 Stocker userIdToAddresses (sans double sérialisation) if (mergedPreferences['userIdToAddresses'] != null) { if (mergedPreferences['userIdToAddresses'] is String) { // Vérifier si la chaîne est déjà un JSON try { - jsonDecode(mergedPreferences['userIdToAddresses']); // Test si c'est déjà un JSON - await prefs.setString('userIdToAddresses', mergedPreferences['userIdToAddresses']); + jsonDecode(mergedPreferences[ + 'userIdToAddresses']); // Test si c'est déjà un JSON + await prefs.setString( + 'userIdToAddresses', mergedPreferences['userIdToAddresses']); } catch (e) { // Sinon, encoder une seule fois - await prefs.setString('userIdToAddresses', jsonEncode(mergedPreferences['userIdToAddresses'])); + await prefs.setString('userIdToAddresses', + jsonEncode(mergedPreferences['userIdToAddresses'])); } } else { - await prefs.setString('userIdToAddresses', jsonEncode(mergedPreferences['userIdToAddresses'])); + await prefs.setString('userIdToAddresses', + jsonEncode(mergedPreferences['userIdToAddresses'])); } } // 🔹 Stocker la devise sélectionnée if (mergedPreferences['selectedCurrency'] != null) { - await prefs.setString('selectedCurrency', mergedPreferences['selectedCurrency']); + await prefs.setString( + 'selectedCurrency', mergedPreferences['selectedCurrency']); } // 🔹 Stocker la préférence de conversion en mètres carrés - await prefs.setBool('convertToSquareMeters', mergedPreferences['convertToSquareMeters'] ?? false); + await prefs.setBool('convertToSquareMeters', + mergedPreferences['convertToSquareMeters'] ?? false); debugPrint("✅ Sauvegarde des préférences terminée !"); } @@ -486,9 +514,11 @@ class GoogleDriveService { } /// Fusionner les données locales et celles de Google Drive - Map _mergeData(Map localData, Map? driveData) { + Map _mergeData( + Map localData, Map? driveData) { if (driveData == null) { - debugPrint("⚠️ Aucune donnée trouvée sur Google Drive, utilisation des données locales."); + debugPrint( + "⚠️ Aucune donnée trouvée sur Google Drive, utilisation des données locales."); return localData; } @@ -498,7 +528,8 @@ class GoogleDriveService { void mergeBox(String boxKey, String backupKey) { if (!driveData.containsKey(backupKey) || driveData[backupKey] == null) { - debugPrint("⚠️ Clé '$backupKey' absente ou vide dans Google Drive, rien à fusionner."); + debugPrint( + "⚠️ Clé '$backupKey' absente ou vide dans Google Drive, rien à fusionner."); return; } @@ -506,11 +537,13 @@ class GoogleDriveService { var driveBoxData = driveData[backupKey]; if (driveBoxData is! Map) { - debugPrint("⚠️ Mauvais format pour '$backupKey', conversion en Map vide."); + debugPrint( + "⚠️ Mauvais format pour '$backupKey', conversion en Map vide."); driveBoxData = {}; } - Map driveBoxMap = Map.from(driveBoxData); + Map driveBoxMap = + Map.from(driveBoxData); driveBoxMap.forEach((key, driveList) { if (key.toString().trim().isEmpty) { @@ -520,10 +553,14 @@ class GoogleDriveService { // Correction si driveList est un Map au lieu d'une liste if (driveList is Map) { - debugPrint("⚠️ Correction : '$key' est une Map, on la transforme en Liste."); - driveList = driveList.entries.map((e) => {'key': e.key, 'value': e.value}).toList(); + debugPrint( + "⚠️ Correction : '$key' est une Map, on la transforme en Liste."); + driveList = driveList.entries + .map((e) => {'key': e.key, 'value': e.value}) + .toList(); } else if (driveList is! List) { - debugPrint("⚠️ '$key' a un type inattendu (${driveList.runtimeType}), conversion en liste vide."); + debugPrint( + "⚠️ '$key' a un type inattendu (${driveList.runtimeType}), conversion en liste vide."); driveList = []; } @@ -532,13 +569,17 @@ class GoogleDriveService { // Vérification si mergedData[boxKey][key] est bien une liste if (mergedData[boxKey][key] is! List) { - debugPrint("⚠️ Correction : '$key' contient un mauvais type dans Hive, conversion en liste vide."); + debugPrint( + "⚠️ Correction : '$key' contient un mauvais type dans Hive, conversion en liste vide."); mergedData[boxKey][key] = []; } List localList = List.from(mergedData[boxKey][key]); - Set existingTimestamps = localList.where((e) => e is Map && e.containsKey('timestamp')).map((e) => e['timestamp'].toString()).toSet(); + Set existingTimestamps = localList + .where((e) => e is Map && e.containsKey('timestamp')) + .map((e) => e['timestamp'].toString()) + .toSet(); if (driveList is List) { for (var entry in driveList) { @@ -566,16 +607,25 @@ class GoogleDriveService { // 🔹 Fusionner les préférences if (driveData.containsKey('preferencesBackup.json')) { debugPrint("🔹 Fusion des préférences..."); - Map drivePreferences = Map.from(driveData['preferencesBackup.json'] ?? {}); - Map localPreferences = Map.from(localData['preferences'] ?? {}); - - Set mergedEthAddresses = {...?localPreferences['ethAddresses'], ...?drivePreferences['ethAddresses']}; + Map drivePreferences = + Map.from(driveData['preferencesBackup.json'] ?? {}); + Map localPreferences = + Map.from(localData['preferences'] ?? {}); + + Set mergedEthAddresses = { + ...?localPreferences['ethAddresses'], + ...?drivePreferences['ethAddresses'] + }; Map mergedPreferences = { 'ethAddresses': mergedEthAddresses.toList(), - 'userIdToAddresses': localPreferences['userIdToAddresses'] ?? drivePreferences['userIdToAddresses'], - 'selectedCurrency': localPreferences['selectedCurrency'] ?? drivePreferences['selectedCurrency'], - 'convertToSquareMeters': localPreferences['convertToSquareMeters'] ?? drivePreferences['convertToSquareMeters'] ?? false, + 'userIdToAddresses': localPreferences['userIdToAddresses'] ?? + drivePreferences['userIdToAddresses'], + 'selectedCurrency': localPreferences['selectedCurrency'] ?? + drivePreferences['selectedCurrency'], + 'convertToSquareMeters': localPreferences['convertToSquareMeters'] ?? + drivePreferences['convertToSquareMeters'] ?? + false, }; mergedData['preferences'] = mergedPreferences; @@ -618,9 +668,11 @@ class GoogleDriveService { final prefs = await SharedPreferences.getInstance(); jsonData['preferencesBackup.json'] = { 'ethAddresses': prefs.getStringList('evmAddresses') ?? [], - 'userIdToAddresses': jsonDecode(prefs.getString('userIdToAddresses') ?? '{}'), // Décoder si nécessaire + 'userIdToAddresses': jsonDecode(prefs.getString('userIdToAddresses') ?? + '{}'), // Décoder si nécessaire 'selectedCurrency': prefs.getString('selectedCurrency'), - 'convertToSquareMeters': prefs.getBool('convertToSquareMeters') ?? false, + 'convertToSquareMeters': + prefs.getBool('convertToSquareMeters') ?? false, }; // Nettoyer les données avant de créer le fichier ZIP @@ -632,7 +684,8 @@ class GoogleDriveService { final archive = Archive(); for (var entry in jsonData.entries) { String jsonContent = jsonEncode(entry.value); - archive.addFile(ArchiveFile(entry.key, jsonContent.length, utf8.encode(jsonContent))); + archive.addFile(ArchiveFile( + entry.key, jsonContent.length, utf8.encode(jsonContent))); } // Sauvegarder le fichier ZIP @@ -663,7 +716,8 @@ class GoogleDriveService { try { debugPrint("🔽 Recherche du fichier sur Google Drive..."); - final drive.FileList fileList = await _driveApi!.files.list(spaces: 'appDataFolder'); + final drive.FileList fileList = + await _driveApi!.files.list(spaces: 'appDataFolder'); if (fileList.files == null || fileList.files!.isEmpty) { debugPrint("❌ Aucun fichier trouvé sur Google Drive !"); @@ -673,7 +727,8 @@ class GoogleDriveService { final String fileId = fileList.files!.first.id!; debugPrint("📂 Fichier trouvé: ID = $fileId, téléchargement en cours..."); - final drive.Media fileData = await _driveApi!.files.get(fileId, downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media; + final drive.Media fileData = await _driveApi!.files.get(fileId, + downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media; final directory = await getApplicationDocumentsDirectory(); final File localFile = File("${directory.path}/realToken_Backup.zip"); @@ -683,7 +738,8 @@ class GoogleDriveService { } await localFile.writeAsBytes(dataStore); - debugPrint("✅ Téléchargement terminé, fichier sauvegardé localement : ${localFile.path}"); + debugPrint( + "✅ Téléchargement terminé, fichier sauvegardé localement : ${localFile.path}"); debugPrint("📦 Extraction et fusion des données..."); await _restoreLocalBackup(localFile.path); diff --git a/lib/settings/advanced_settings_page.dart b/lib/settings/advanced_settings_page.dart index 535658b..10e8f2a 100644 --- a/lib/settings/advanced_settings_page.dart +++ b/lib/settings/advanced_settings_page.dart @@ -49,7 +49,8 @@ class _AdvancedSettingsPageState extends State { padding: EdgeInsets.zero, children: [ const SizedBox(height: 12), - _buildSectionHeader(context, "Historique YAM", CupertinoIcons.chart_bar_alt_fill), + _buildSectionHeader( + context, "Historique YAM", CupertinoIcons.chart_bar_alt_fill), _buildSettingsSection( context, children: [ @@ -57,7 +58,8 @@ class _AdvancedSettingsPageState extends State { context, title: S.of(context).yamHistory, subtitle: "${S.of(context).daysLimit}: $_daysLimit days", - trailing: const Icon(CupertinoIcons.pencil, size: 14, color: Colors.grey), + trailing: const Icon(CupertinoIcons.pencil, + size: 14, color: Colors.grey), onTap: () => _showNumberPicker(context), isFirst: true, isLast: true, @@ -65,16 +67,23 @@ class _AdvancedSettingsPageState extends State { ], ), const SizedBox(height: 12), - _buildSectionHeader(context, "Réactivité APY", CupertinoIcons.waveform_path), + _buildSectionHeader( + context, "Réactivité APY", CupertinoIcons.waveform_path), _buildSettingsSection( context, - footnote: 'Ajustez la sensibilité du calcul d\'APY aux variations récentes', + footnote: + 'Ajustez la sensibilité du calcul d\'APY aux variations récentes', children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), child: Row( children: [ - Text('Lisse', style: TextStyle(fontSize: 13 + Provider.of(context, listen: false).getTextSizeOffset())), + Text('Lisse', + style: TextStyle( + fontSize: 13 + + Provider.of(context, listen: false) + .getTextSizeOffset())), Expanded( child: Slider.adaptive( value: _apyReactivity, @@ -109,14 +118,16 @@ class _AdvancedSettingsPageState extends State { ], ), const SizedBox(height: 12), - _buildSectionHeader(context, "Gestion des données", CupertinoIcons.delete), + _buildSectionHeader( + context, "Gestion des données", CupertinoIcons.delete), _buildSettingsSection( context, children: [ _buildSettingsItem( context, title: 'Effacer le cache et les données', - trailing: const Icon(CupertinoIcons.delete, color: Colors.red, size: 14), + trailing: const Icon(CupertinoIcons.delete, + color: Colors.red, size: 14), onTap: () => _showDeleteConfirmation(context), isFirst: true, isLast: true, @@ -129,7 +140,8 @@ class _AdvancedSettingsPageState extends State { ); } - Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(left: 16, bottom: 6, top: 2), child: Row( @@ -165,7 +177,7 @@ class _AdvancedSettingsPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -253,7 +265,10 @@ class _AdvancedSettingsPageState extends State { if (!isLast) Padding( padding: const EdgeInsets.only(left: 12), - child: Divider(height: 1, thickness: 0.5, color: Colors.grey.withOpacity(0.3)), + child: Divider( + height: 1, + thickness: 0.5, + color: Colors.grey.withValues(alpha: 0.3)), ), ], ); @@ -264,7 +279,8 @@ class _AdvancedSettingsPageState extends State { context: context, builder: (context) => CupertinoAlertDialog( title: const Text('Confirmer la suppression'), - content: const Text('Êtes-vous sûr de vouloir effacer toutes les données et le cache? Cette action est irréversible.'), + content: const Text( + 'Êtes-vous sûr de vouloir effacer toutes les données et le cache? Cette action est irréversible.'), actions: [ CupertinoDialogAction( isDefaultAction: true, @@ -313,10 +329,12 @@ class _AdvancedSettingsPageState extends State { child: Column( children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: Colors.grey.withOpacity(0.3), width: 0.5), + bottom: BorderSide( + color: Colors.grey.withValues(alpha: 0.3), width: 0.5), ), ), child: Row( @@ -325,7 +343,9 @@ class _AdvancedSettingsPageState extends State { CupertinoButton( padding: EdgeInsets.zero, onPressed: () => Navigator.pop(context), - child: Text("Annuler", style: TextStyle(color: CupertinoColors.destructiveRed)), + child: Text("Annuler", + style: + TextStyle(color: CupertinoColors.destructiveRed)), ), Text( "Jours d'historique", @@ -354,7 +374,8 @@ class _AdvancedSettingsPageState extends State { onSelectedItemChanged: (index) { tempDaysLimit = index + 1; }, - scrollController: FixedExtentScrollController(initialItem: _daysLimit - 1), + scrollController: + FixedExtentScrollController(initialItem: _daysLimit - 1), children: List.generate(365, (index) { return Center( child: Text( diff --git a/lib/settings/appearance_settings_page.dart b/lib/settings/appearance_settings_page.dart index 850508c..3bad27d 100644 --- a/lib/settings/appearance_settings_page.dart +++ b/lib/settings/appearance_settings_page.dart @@ -128,7 +128,7 @@ class _AppearanceSettingsPageState extends State { : null, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 1, offset: const Offset(0, 1), ), @@ -237,7 +237,7 @@ class _AppearanceSettingsPageState extends State { decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: Colors.grey.withOpacity(0.3), width: 0.5)), + color: Colors.grey.withValues(alpha: 0.3), width: 0.5)), color: isDarkMode(context) ? const Color(0xFF2C2C2E) : Colors.white, @@ -247,16 +247,12 @@ class _AppearanceSettingsPageState extends State { children: [ CupertinoButton( padding: EdgeInsets.zero, - child: Text("Annuler", - style: TextStyle( - fontSize: 14)), + child: Text("Annuler", style: TextStyle(fontSize: 14)), onPressed: () => Navigator.of(context).pop(), ), CupertinoButton( padding: EdgeInsets.zero, - child: Text("OK", - style: TextStyle( - fontSize: 14)), + child: Text("OK", style: TextStyle(fontSize: 14)), onPressed: () => Navigator.of(context).pop(), ), ], @@ -304,7 +300,7 @@ class _AppearanceSettingsPageState extends State { decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: Colors.grey.withOpacity(0.3), width: 0.5)), + color: Colors.grey.withValues(alpha: 0.3), width: 0.5)), color: isDarkMode(context) ? const Color(0xFF2C2C2E) : Colors.white, @@ -314,16 +310,12 @@ class _AppearanceSettingsPageState extends State { children: [ CupertinoButton( padding: EdgeInsets.zero, - child: Text("Annuler", - style: TextStyle( - fontSize: 14)), + child: Text("Annuler", style: TextStyle(fontSize: 14)), onPressed: () => Navigator.of(context).pop(), ), CupertinoButton( padding: EdgeInsets.zero, - child: Text("OK", - style: TextStyle( - fontSize: 14)), + child: Text("OK", style: TextStyle(fontSize: 14)), onPressed: () => Navigator.of(context).pop(), ), ], @@ -396,7 +388,7 @@ class _AppearanceSettingsPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -456,7 +448,9 @@ class _AppearanceSettingsPageState extends State { Padding( padding: const EdgeInsets.only(left: 12), child: Divider( - height: 1, thickness: 0.5, color: Colors.grey.withOpacity(0.3)), + height: 1, + thickness: 0.5, + color: Colors.grey.withValues(alpha: 0.3)), ), ], ); @@ -471,12 +465,12 @@ class _AppearanceSettingsPageState extends State { padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: isDark - ? Colors.grey.shade800.withOpacity(0.3) - : Colors.grey.shade200.withOpacity(0.7), + ? Colors.grey.shade800.withValues(alpha: 0.3) + : Colors.grey.shade200.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), @@ -501,7 +495,7 @@ class _AppearanceSettingsPageState extends State { borderRadius: BorderRadius.circular(26), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(0, 1), ), @@ -566,7 +560,7 @@ class _AppearanceSettingsPageState extends State { required VoidCallback onTap}) { final textColor = isSelected ? Theme.of(context).primaryColor - : Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6); + : Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.6); return GestureDetector( onTap: onTap, diff --git a/lib/settings/biometric_settings_page.dart b/lib/settings/biometric_settings_page.dart index 3e5290e..1fd8c04 100644 --- a/lib/settings/biometric_settings_page.dart +++ b/lib/settings/biometric_settings_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/cupertino.dart'; import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:realtoken_asset_tracker/services/biometric_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:app_settings/app_settings.dart'; @@ -40,7 +39,8 @@ class _BiometricSettingsPageState extends State { try { final isAvailable = await _biometricService.isBiometricAvailable(); final isEnabled = await _biometricService.isBiometricEnabled(); - final availableBiometrics = await _biometricService.getAvailableBiometrics(); + final availableBiometrics = + await _biometricService.getAvailableBiometrics(); final biometricType = await _biometricService.getBiometricTypeName(); setState(() { @@ -48,7 +48,9 @@ class _BiometricSettingsPageState extends State { _isBiometricEnabled = isEnabled; _availableBiometrics = availableBiometrics; _biometricType = biometricType; - _statusMessage = isAvailable ? 'Votre appareil prend en charge $_biometricType' : 'Votre appareil ne prend pas en charge l\'authentification biométrique'; + _statusMessage = isAvailable + ? 'Votre appareil prend en charge $_biometricType' + : 'Votre appareil ne prend pas en charge l\'authentification biométrique'; _isLoading = false; }); } catch (e) { @@ -74,10 +76,13 @@ class _BiometricSettingsPageState extends State { }); try { - final authenticated = await _biometricService.authenticate(reason: 'Ceci est un test d\'authentification biométrique'); + final authenticated = await _biometricService.authenticate( + reason: 'Ceci est un test d\'authentification biométrique'); setState(() { - _statusMessage = authenticated ? 'Test réussi! L\'authentification biométrique fonctionne correctement.' : 'Échec du test. Veuillez réessayer.'; + _statusMessage = authenticated + ? 'Test réussi! L\'authentification biométrique fonctionne correctement.' + : 'Échec du test. Veuillez réessayer.'; _isTesting = false; }); } catch (e) { @@ -121,9 +126,13 @@ class _BiometricSettingsPageState extends State { children: [ const SizedBox(height: 12), _buildSectionHeader(context, "Sécurité", CupertinoIcons.lock), - if (!_isBiometricAvailable) _buildErrorCard() else _buildSettingsCard(), + if (!_isBiometricAvailable) + _buildErrorCard() + else + _buildSettingsCard(), const SizedBox(height: 24), - if (_isBiometricAvailable && _isBiometricEnabled) _buildTestSection(), + if (_isBiometricAvailable && _isBiometricEnabled) + _buildTestSection(), ], ), ); @@ -134,9 +143,9 @@ class _BiometricSettingsPageState extends State { margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.withOpacity(0.3)), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -170,7 +179,9 @@ class _BiometricSettingsPageState extends State { AppSettings.openAppSettings(type: AppSettingsType.security); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Impossible d\'ouvrir les paramètres. Veuillez les configurer manuellement.')), + SnackBar( + content: Text( + 'Impossible d\'ouvrir les paramètres. Veuillez les configurer manuellement.')), ); } }, @@ -229,14 +240,15 @@ class _BiometricSettingsPageState extends State { ), CupertinoSwitch( value: _isBiometricEnabled, - activeColor: Theme.of(context).primaryColor, + activeTrackColor: Theme.of(context).primaryColor, onChanged: (bool value) async { if (value) { await _authenticateWithBiometrics(); } else { await _saveSetting(false); setState(() { - _statusMessage = 'Authentification biométrique désactivée'; + _statusMessage = + 'Authentification biométrique désactivée'; }); } }, @@ -259,7 +271,7 @@ class _BiometricSettingsPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -292,7 +304,8 @@ class _BiometricSettingsPageState extends State { icon: Icon(_getBiometricIcon()), label: Text('Tester maintenant'), style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: + EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ), @@ -306,7 +319,8 @@ class _BiometricSettingsPageState extends State { _statusMessage = 'Authentification en cours...'; }); - final authenticated = await _biometricService.authenticate(reason: 'Authentifiez-vous pour activer la biométrie'); + final authenticated = await _biometricService.authenticate( + reason: 'Authentifiez-vous pour activer la biométrie'); if (authenticated) { await _saveSetting(true); @@ -333,7 +347,8 @@ class _BiometricSettingsPageState extends State { } } - Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(left: 16, bottom: 6, top: 2), child: Row( @@ -354,7 +369,8 @@ class _BiometricSettingsPageState extends State { ); } - Widget _buildSettingsSection(BuildContext context, {required List children}) { + Widget _buildSettingsSection(BuildContext context, + {required List children}) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( @@ -362,7 +378,7 @@ class _BiometricSettingsPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), diff --git a/lib/settings/manage_evm_addresses_page.dart b/lib/settings/manage_evm_addresses_page.dart index e90763b..1fc59cd 100644 --- a/lib/settings/manage_evm_addresses_page.dart +++ b/lib/settings/manage_evm_addresses_page.dart @@ -28,12 +28,15 @@ class ManageEvmAddressesPageState extends State { // Charger les relations userId-adresses WidgetsBinding.instance.addPostFrameCallback((_) { final dataManager = Provider.of(context, listen: false); - debugPrint("🔑 ManageEvmAddresses: chargement des relations userId-adresses"); - dataManager.loadUserIdToAddresses(); // Charger les relations userId-adresses - + debugPrint( + "🔑 ManageEvmAddresses: chargement des relations userId-adresses"); + dataManager + .loadUserIdToAddresses(); // Charger les relations userId-adresses + // Forcé la mise à jour des données car c'est une page de gestion d'adresses // On a besoin des données les plus récentes ici - debugPrint("🔑 ManageEvmAddresses: forçage de la mise à jour des données"); + debugPrint( + "🔑 ManageEvmAddresses: forçage de la mise à jour des données"); DataFetchUtils.refreshData(context); }); } @@ -72,7 +75,8 @@ class ManageEvmAddressesPageState extends State { final dataManager = Provider.of(context, listen: false); // Utilisation de la nouvelle API pour récupérer le userId et les adresses associées - final result = await ApiService.fetchUserAndAddresses(address.toLowerCase()); + final result = + await ApiService.fetchUserAndAddresses(address.toLowerCase()); if (result != null) { final String userId = result['userId']; @@ -90,7 +94,8 @@ class ManageEvmAddressesPageState extends State { } // Forcer la mise à jour des données après l'ajout - debugPrint("🔑 ManageEvmAddresses: forçage de la mise à jour après ajout d'adresse"); + debugPrint( + "🔑 ManageEvmAddresses: forçage de la mise à jour après ajout d'adresse"); DataFetchUtils.refreshData(context); } } @@ -154,13 +159,19 @@ class ManageEvmAddressesPageState extends State { @override Widget build(BuildContext context) { final dataManager = Provider.of(context); - final appState = Provider.of(context); // Récupérer AppState pour le texte + final appState = + Provider.of(context); // Récupérer AppState pour le texte // Récupérer toutes les adresses liées à un userId - final List linkedAddresses = dataManager.getAllUserIds().expand((userId) => dataManager.getAddressesForUserId(userId) ?? []).toList(); + final List linkedAddresses = dataManager + .getAllUserIds() + .expand((userId) => dataManager.getAddressesForUserId(userId) ?? []) + .toList(); // Filtrer les adresses non liées - final unlinkedAddresses = evmAddresses.where((address) => !linkedAddresses.contains(address)).toList(); + final unlinkedAddresses = evmAddresses + .where((address) => !linkedAddresses.contains(address)) + .toList(); return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -177,7 +188,8 @@ class ManageEvmAddressesPageState extends State { padding: EdgeInsets.zero, children: [ const SizedBox(height: 12), - _buildSectionHeader(context, S.of(context).addAddress, CupertinoIcons.plus_circle), + _buildSectionHeader( + context, S.of(context).addAddress, CupertinoIcons.plus_circle), Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(12), @@ -186,7 +198,7 @@ class ManageEvmAddressesPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -200,12 +212,14 @@ class ManageEvmAddressesPageState extends State { Expanded( child: CupertinoTextField( controller: _evmAddressController, - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 8), placeholder: S.of(context).walletAddress, decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.withOpacity(0.3)), + border: Border.all( + color: Colors.grey.withValues(alpha: 0.3)), ), ), ), @@ -220,7 +234,8 @@ class ManageEvmAddressesPageState extends State { color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(8), ), - child: const Icon(CupertinoIcons.qrcode_viewfinder, color: Colors.white, size: 20), + child: const Icon(CupertinoIcons.qrcode_viewfinder, + color: Colors.white, size: 20), ), ), ], @@ -239,7 +254,8 @@ class ManageEvmAddressesPageState extends State { _evmAddressController.clear(); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Invalid wallet address')), + const SnackBar( + content: Text('Invalid wallet address')), ); } }, @@ -257,7 +273,8 @@ class ManageEvmAddressesPageState extends State { ), if (unlinkedAddresses.isNotEmpty) ...[ const SizedBox(height: 16), - _buildSectionHeader(context, "Adresses non associées", CupertinoIcons.creditcard), + _buildSectionHeader( + context, "Adresses non associées", CupertinoIcons.creditcard), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -275,7 +292,8 @@ class ManageEvmAddressesPageState extends State { ], if (dataManager.getAllUserIds().isNotEmpty) ...[ const SizedBox(height: 16), - _buildSectionHeader(context, "Adresses associées", CupertinoIcons.person_crop_circle), + _buildSectionHeader(context, "Adresses associées", + CupertinoIcons.person_crop_circle), for (final userId in dataManager.getAllUserIds()) _buildUserSection( context, @@ -290,7 +308,8 @@ class ManageEvmAddressesPageState extends State { ); } - Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(left: 16, bottom: 6, top: 2), child: Row( @@ -325,7 +344,7 @@ class ManageEvmAddressesPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -345,7 +364,8 @@ class ManageEvmAddressesPageState extends State { ), ), IconButton( - icon: Icon(CupertinoIcons.doc_on_clipboard, color: Theme.of(context).primaryColor, size: 20), + icon: Icon(CupertinoIcons.doc_on_clipboard, + color: Theme.of(context).primaryColor, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { @@ -357,7 +377,8 @@ class ManageEvmAddressesPageState extends State { ), const SizedBox(width: 12), IconButton( - icon: const Icon(CupertinoIcons.delete, color: Colors.red, size: 20), + icon: const Icon(CupertinoIcons.delete, + color: Colors.red, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: onDelete, @@ -383,7 +404,7 @@ class ManageEvmAddressesPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -402,7 +423,9 @@ class ManageEvmAddressesPageState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( @@ -422,7 +445,8 @@ class ManageEvmAddressesPageState extends State { ], ), IconButton( - icon: const Icon(CupertinoIcons.delete, color: Colors.red, size: 20), + icon: const Icon(CupertinoIcons.delete, + color: Colors.red, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { @@ -440,7 +464,8 @@ class ManageEvmAddressesPageState extends State { children: [ if (i == 0) const Divider(height: 1, thickness: 0.5), Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ Expanded( @@ -453,25 +478,32 @@ class ManageEvmAddressesPageState extends State { ), ), IconButton( - icon: Icon(CupertinoIcons.doc_on_clipboard, color: Theme.of(context).primaryColor, size: 20), + icon: Icon(CupertinoIcons.doc_on_clipboard, + color: Theme.of(context).primaryColor, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { Clipboard.setData(ClipboardData(text: addresses[i])); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Address copied: ${addresses[i]}')), + SnackBar( + content: + Text('Address copied: ${addresses[i]}')), ); }, ), const SizedBox(width: 12), IconButton( - icon: const Icon(CupertinoIcons.delete, color: Colors.red, size: 20), + icon: const Icon(CupertinoIcons.delete, + color: Colors.red, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { - dataManager.removeAddressForUserId(userId, addresses[i]); + dataManager.removeAddressForUserId( + userId, addresses[i]); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Address ${addresses[i]} deleted')), + SnackBar( + content: + Text('Address ${addresses[i]} deleted')), ); }, ), @@ -481,7 +513,10 @@ class ManageEvmAddressesPageState extends State { if (i < addresses.length - 1) Padding( padding: const EdgeInsets.only(left: 12), - child: Divider(height: 1, thickness: 0.5, color: Colors.grey.withOpacity(0.3)), + child: Divider( + height: 1, + thickness: 0.5, + color: Colors.grey.withValues(alpha: 0.3)), ), ], ), diff --git a/lib/settings/notifications_settings_page.dart b/lib/settings/notifications_settings_page.dart index 4d73d77..8680b9e 100644 --- a/lib/settings/notifications_settings_page.dart +++ b/lib/settings/notifications_settings_page.dart @@ -11,7 +11,8 @@ class NotificationsSettingsPage extends StatefulWidget { const NotificationsSettingsPage({super.key}); @override - _NotificationsSettingsPageState createState() => _NotificationsSettingsPageState(); + _NotificationsSettingsPageState createState() => + _NotificationsSettingsPageState(); } class _NotificationsSettingsPageState extends State { @@ -27,8 +28,9 @@ class _NotificationsSettingsPageState extends State { Future _loadNotificationStatus() async { final prefs = await SharedPreferences.getInstance(); bool isSubscribed = OneSignal.User.pushSubscription.optedIn ?? false; - bool hasRefused = prefs.getBool(PreferenceKeys.hasRefusedNotifications) ?? false; - + bool hasRefused = + prefs.getBool(PreferenceKeys.hasRefusedNotifications) ?? false; + setState(() { _notificationsEnabled = isSubscribed; _hasRefusedNotifications = hasRefused; @@ -57,7 +59,7 @@ class _NotificationsSettingsPageState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(PreferenceKeys.hasRefusedNotifications, false); await prefs.setBool(PreferenceKeys.hasAskedNotifications, false); - + setState(() { _hasRefusedNotifications = false; }); @@ -65,18 +67,20 @@ class _NotificationsSettingsPageState extends State { // Redemander la permission final hasPermission = await OneSignal.Notifications.requestPermission(true); await prefs.setBool(PreferenceKeys.hasAskedNotifications, true); - + if (hasPermission) { setState(() { _notificationsEnabled = true; }); - debugPrint("✅ Permissions de notifications accordées après réinitialisation"); + debugPrint( + "✅ Permissions de notifications accordées après réinitialisation"); } else { await prefs.setBool(PreferenceKeys.hasRefusedNotifications, true); setState(() { _hasRefusedNotifications = true; }); - debugPrint("🚫 Permissions de notifications refusées après réinitialisation"); + debugPrint( + "🚫 Permissions de notifications refusées après réinitialisation"); } } @@ -131,7 +135,8 @@ class _NotificationsSettingsPageState extends State { ); } - Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(left: 16, bottom: 6, top: 2), child: Row( @@ -163,7 +168,7 @@ class _NotificationsSettingsPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -230,7 +235,7 @@ class _NotificationsSettingsPageState extends State { child: CupertinoSwitch( value: value, onChanged: onChanged, - activeColor: Theme.of(context).primaryColor, + activeTrackColor: Theme.of(context).primaryColor, ), ), ], @@ -245,9 +250,9 @@ class _NotificationsSettingsPageState extends State { margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.1), + color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.orange.withOpacity(0.3)), + border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -296,7 +301,9 @@ class _NotificationsSettingsPageState extends State { decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: !isLast ? Colors.grey.withOpacity(0.2) : Colors.transparent, + color: !isLast + ? Colors.grey.withValues(alpha: 0.2) + : Colors.transparent, width: 0.5, ), ), diff --git a/lib/settings/personalization_settings_page.dart b/lib/settings/personalization_settings_page.dart index 2ed4c82..4417da0 100644 --- a/lib/settings/personalization_settings_page.dart +++ b/lib/settings/personalization_settings_page.dart @@ -13,13 +13,16 @@ class PersonalizationSettingsPage extends StatefulWidget { const PersonalizationSettingsPage({super.key}); @override - _PersonalizationSettingsPageState createState() => _PersonalizationSettingsPageState(); + _PersonalizationSettingsPageState createState() => + _PersonalizationSettingsPageState(); } -class _PersonalizationSettingsPageState extends State { +class _PersonalizationSettingsPageState + extends State { Map _currencies = {}; final TextEditingController _adjustmentController = TextEditingController(); - final TextEditingController _initialInvestmentAdjustmentController = TextEditingController(); + final TextEditingController _initialInvestmentAdjustmentController = + TextEditingController(); Future _saveConvertToSquareMeters(bool value) async { final prefs = await SharedPreferences.getInstance(); @@ -72,7 +75,8 @@ class _PersonalizationSettingsPageState extends State(context, listen: false).getTextSizeOffset(), + fontSize: 14.0 + + Provider.of(context, + listen: false) + .getTextSizeOffset(), color: Colors.grey, ), ), - const Icon(CupertinoIcons.chevron_right, size: 14, color: Colors.grey), + const Icon(CupertinoIcons.chevron_right, + size: 14, color: Colors.grey), ], ), - onPressed: () => _showCurrencyPicker(context, currencyProvider), + onPressed: () => + _showCurrencyPicker(context, currencyProvider), ); }, ) @@ -192,7 +205,8 @@ class _PersonalizationSettingsPageState extends State(context, listen: false); showCupertinoModalPopup( @@ -536,8 +575,12 @@ class _PersonalizationSettingsPageState extends State Center( @@ -605,11 +651,14 @@ class _PersonalizationSettingsPageState extends State _navigateTo(context, PersonalizationSettingsPage()), + onTap: () => + _navigateTo(context, PersonalizationSettingsPage()), ), _buildSettingsItem( context, icon: CupertinoIcons.arrow_2_circlepath, color: Colors.blue, title: S.of(context).synchronization, - onTap: () => _navigateTo(context, SynchronizationSettingsPage()), + onTap: () => + _navigateTo(context, SynchronizationSettingsPage()), ), _buildSettingsItem( context, @@ -81,7 +84,8 @@ class SettingsPage extends StatelessWidget { ); } - Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(left: 16, bottom: 6, top: 2), child: Row( @@ -102,7 +106,8 @@ class SettingsPage extends StatelessWidget { ); } - Widget _buildSettingsSection(BuildContext context, {required List children}) { + Widget _buildSettingsSection(BuildContext context, + {required List children}) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( @@ -110,7 +115,7 @@ class SettingsPage extends StatelessWidget { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -153,7 +158,7 @@ class SettingsPage extends StatelessWidget { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, size: 18, color: color), @@ -168,7 +173,8 @@ class SettingsPage extends StatelessWidget { ), ), ), - const Icon(CupertinoIcons.chevron_right, size: 14, color: Colors.grey), + const Icon(CupertinoIcons.chevron_right, + size: 14, color: Colors.grey), ], ), ), @@ -176,7 +182,10 @@ class SettingsPage extends StatelessWidget { if (!isLast) Padding( padding: const EdgeInsets.only(left: 46), - child: Divider(height: 1, thickness: 0.5, color: Colors.grey.withOpacity(0.3)), + child: Divider( + height: 1, + thickness: 0.5, + color: Colors.grey.withValues(alpha: 0.3)), ), ], ); diff --git a/lib/settings/synchronization_settings_page.dart b/lib/settings/synchronization_settings_page.dart index 47a4e46..fd2eb68 100644 --- a/lib/settings/synchronization_settings_page.dart +++ b/lib/settings/synchronization_settings_page.dart @@ -22,10 +22,12 @@ class SynchronizationSettingsPage extends StatefulWidget { const SynchronizationSettingsPage({super.key}); @override - _SynchronizationSettingsPageState createState() => _SynchronizationSettingsPageState(); + _SynchronizationSettingsPageState createState() => + _SynchronizationSettingsPageState(); } -class _SynchronizationSettingsPageState extends State { +class _SynchronizationSettingsPageState + extends State { final GoogleDriveService _googleDriveService = GoogleDriveService(); bool _isGoogleDriveConnected = false; bool _autoSyncEnabled = false; @@ -100,22 +102,28 @@ class _SynchronizationSettingsPageState extends State(context, listen: false).getTextSizeOffset(), + fontSize: 15 + + Provider.of(context, listen: false) + .getTextSizeOffset(), fontWeight: FontWeight.w500, ), ), CupertinoButton( padding: EdgeInsets.zero, - child: const Icon(CupertinoIcons.info_circle, size: 20, color: Colors.grey), + child: const Icon(CupertinoIcons.info_circle, + size: 20, color: Colors.grey), onPressed: () { showDialog( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( - title: Text(S.of(context).aboutImportExportTitle), - content: Text(S.of(context).aboutImportExport), + title: Text( + S.of(context).aboutImportExportTitle), + content: + Text(S.of(context).aboutImportExport), actions: [ CupertinoDialogAction( isDefaultAction: true, @@ -195,7 +210,8 @@ class _SynchronizationSettingsPageState extends State shareZippedHiveData(), @@ -208,7 +224,8 @@ class _SynchronizationSettingsPageState extends State importZippedHiveData(), @@ -232,7 +249,8 @@ class _SynchronizationSettingsPageState extends State ethAddresses = prefs.getStringList('evmAddresses') ?? []; String? userIdToAddresses = prefs.getString('userIdToAddresses'); String? selectedCurrency = prefs.getString('selectedCurrency'); - bool convertToSquareMeters = prefs.getBool('convertToSquareMeters') ?? false; + bool convertToSquareMeters = + prefs.getBool('convertToSquareMeters') ?? false; Map preferencesData = { 'ethAddresses': ethAddresses, 'userIdToAddresses': userIdToAddresses, @@ -409,14 +437,22 @@ class _SynchronizationSettingsPageState extends State recordsToSave; @@ -566,33 +614,48 @@ class _SynchronizationSettingsPageState extends State v is List ? v : [v]).toList(); - debugPrint("⚠️ Clé 'balanceHistory_totalWalletValue' non trouvée, utilisation de l'ensemble des valeurs: ${recordsToSave.length} enregistrements"); + recordsToSave = decodedData.values + .expand((v) => v is List ? v : [v]) + .toList(); + debugPrint( + "⚠️ Clé 'balanceHistory_totalWalletValue' non trouvée, utilisation de l'ensemble des valeurs: ${recordsToSave.length} enregistrements"); } } else if (decodedData is List) { - debugPrint("📋 Données reçues sous forme de Liste avec ${decodedData.length} éléments"); + debugPrint( + "📋 Données reçues sous forme de Liste avec ${decodedData.length} éléments"); recordsToSave = decodedData; } else { - debugPrint("⚠️ Format de données non reconnu: ${decodedData.runtimeType}"); + debugPrint( + "⚠️ Format de données non reconnu: ${decodedData.runtimeType}"); throw Exception("Format de données non supporté"); } // Convertir les enregistrements en objets BalanceRecord - List balanceRecords = recordsToSave.map((recordJson) => BalanceRecord.fromJson(Map.from(recordJson))).toList(); + List balanceRecords = recordsToSave + .map((recordJson) => BalanceRecord.fromJson( + Map.from(recordJson))) + .toList(); - debugPrint("✅ Conversion effectuée: ${balanceRecords.length} objets BalanceRecord créés"); + debugPrint( + "✅ Conversion effectuée: ${balanceRecords.length} objets BalanceRecord créés"); // Convertir en JSON pour la sauvegarde - List> balanceHistoryJsonToSave = balanceRecords.map((record) => record.toJson()).toList(); + List> balanceHistoryJsonToSave = + balanceRecords.map((record) => record.toJson()).toList(); // Sauvegarder dans les deux boîtes - await walletValueArchiveBox.put('balanceHistory_totalWalletValue', balanceHistoryJsonToSave); - await balanceHistoryBox.put('balanceHistory_totalWalletValue', balanceHistoryJsonToSave); + await walletValueArchiveBox.put( + 'balanceHistory_totalWalletValue', balanceHistoryJsonToSave); + await balanceHistoryBox.put( + 'balanceHistory_totalWalletValue', balanceHistoryJsonToSave); debugPrint("✅ Sauvegarde terminée dans les deux boîtes Hive"); @@ -600,12 +663,15 @@ class _SynchronizationSettingsPageState extends State(context, listen: false); if (appState.dataManager != null) { appState.dataManager!.walletBalanceHistory = balanceRecords; - appState.dataManager!.balanceHistory = List.from(balanceRecords); + appState.dataManager!.balanceHistory = + List.from(balanceRecords); appState.dataManager!.notifyListeners(); - debugPrint("✅ DataManager mis à jour avec ${balanceRecords.length} enregistrements"); + debugPrint( + "✅ DataManager mis à jour avec ${balanceRecords.length} enregistrements"); } } catch (e) { - debugPrint("❌ Erreur lors de l'importation de walletValueArchiveBackup.json: $e"); + debugPrint( + "❌ Erreur lors de l'importation de walletValueArchiveBackup.json: $e"); rethrow; } } else if (file.name == 'customInitPricesBackup.json') { @@ -630,8 +696,10 @@ class _SynchronizationSettingsPageState extends State customHealthAndLtvData = jsonDecode(jsonContent); - var customHealthAndLtvBox = await Hive.openBox('HealthAndLtvValueArchive'); + Map customHealthAndLtvData = + jsonDecode(jsonContent); + var customHealthAndLtvBox = + await Hive.openBox('HealthAndLtvValueArchive'); await customHealthAndLtvBox.putAll(customHealthAndLtvData); } else if (file.name == 'preferencesBackup.json') { // Décoder et insérer les préférences dans SharedPreferences @@ -639,15 +707,19 @@ class _SynchronizationSettingsPageState extends State ethAddresses = List.from(preferencesData['ethAddresses'] ?? []); + List ethAddresses = + List.from(preferencesData['ethAddresses'] ?? []); String? userIdToAddresses = preferencesData['userIdToAddresses']; String? selectedCurrency = preferencesData['selectedCurrency']; - bool convertToSquareMeters = preferencesData['convertToSquareMeters'] ?? false; + bool convertToSquareMeters = + preferencesData['convertToSquareMeters'] ?? false; // Sauvegarder les préférences restaurées await prefs.setStringList('evmAddresses', ethAddresses); - if (userIdToAddresses != null) await prefs.setString('userIdToAddresses', userIdToAddresses); - if (selectedCurrency != null) await prefs.setString('selectedCurrency', selectedCurrency); + if (userIdToAddresses != null) + await prefs.setString('userIdToAddresses', userIdToAddresses); + if (selectedCurrency != null) + await prefs.setString('selectedCurrency', selectedCurrency); await prefs.setBool('convertToSquareMeters', convertToSquareMeters); } } @@ -662,7 +734,8 @@ class _SynchronizationSettingsPageState extends State[ - BottomNavigationBarItem(icon: const Icon(Icons.dashboard), label: S.of(context).dashboard), - BottomNavigationBarItem(icon: const Icon(Icons.wallet_rounded), label: S.of(context).portfolio), - BottomNavigationBarItem(icon: const Icon(Icons.bar_chart), label: S.of(context).analytics), - BottomNavigationBarItem(icon: const Icon(Icons.map), label: S.of(context).maps), + BottomNavigationBarItem( + icon: const Icon(Icons.dashboard), label: S.of(context).dashboard), + BottomNavigationBarItem( + icon: const Icon(Icons.wallet_rounded), + label: S.of(context).portfolio), + BottomNavigationBarItem( + icon: const Icon(Icons.bar_chart), label: S.of(context).analytics), + BottomNavigationBarItem( + icon: const Icon(Icons.map), label: S.of(context).maps), ], currentIndex: selectedIndex, elevation: 0, diff --git a/lib/structure/drawer.dart b/lib/structure/drawer.dart index 88b0706..8c73e26 100644 --- a/lib/structure/drawer.dart +++ b/lib/structure/drawer.dart @@ -47,10 +47,13 @@ class _CustomDrawerState extends State { Future _fetchVersions() async { // Throttle pour éviter les appels répétitifs - if (!PerformanceUtils.throttle('drawer_version_check', const Duration(minutes: 30))) { + if (!PerformanceUtils.throttle( + 'drawer_version_check', const Duration(minutes: 30))) { // Charger depuis le cache si disponible - final cachedCurrent = PerformanceUtils.getFromCache('current_version'); - final cachedLatest = PerformanceUtils.getFromCache('latest_version'); + final cachedCurrent = + PerformanceUtils.getFromCache('current_version'); + final cachedLatest = + PerformanceUtils.getFromCache('latest_version'); if (cachedCurrent != null && cachedLatest != null) { setState(() { currentVersion = cachedCurrent; @@ -64,15 +67,17 @@ class _CustomDrawerState extends State { // Récupérer la version actuelle de l'application final packageInfo = await PackageInfo.fromPlatform(); final currentVer = packageInfo.version; - + // Mettre en cache et setState pour la version actuelle - PerformanceUtils.setCache('current_version', currentVer, CacheConstants.versionCache); + PerformanceUtils.setCache( + 'current_version', currentVer, CacheConstants.versionCache); setState(() { currentVersion = currentVer; }); // Vérifier le cache pour la version latest d'abord - final cachedLatest = PerformanceUtils.getFromCache('latest_version'); + final cachedLatest = + PerformanceUtils.getFromCache('latest_version'); if (cachedLatest != null) { setState(() { latestVersion = cachedLatest; @@ -82,17 +87,20 @@ class _CustomDrawerState extends State { // Récupérer la dernière version depuis GitHub seulement si pas en cache final response = await http.get( - Uri.parse('https://api.github.com/repos/RealToken-Community/realtoken_apps/releases/latest'), + Uri.parse( + 'https://api.github.com/repos/RealToken-Community/realtoken_apps/releases/latest'), headers: {'Accept': 'application/vnd.github.v3+json'}, ).timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { final releaseData = json.decode(response.body); final tagName = releaseData['tag_name']; - final latestVer = tagName.startsWith('v') ? tagName.substring(1) : tagName; + final latestVer = + tagName.startsWith('v') ? tagName.substring(1) : tagName; // Mettre en cache et setState - PerformanceUtils.setCache('latest_version', latestVer, CacheConstants.versionCache); + PerformanceUtils.setCache( + 'latest_version', latestVer, CacheConstants.versionCache); if (mounted) { setState(() { latestVersion = latestVer; @@ -139,7 +147,10 @@ class _CustomDrawerState extends State { ), content: Text( S.of(context).reviewRequestUnavailable, - style: TextStyle(fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset()), + style: TextStyle( + fontSize: 14 + + Provider.of(context, listen: false) + .getTextSizeOffset()), ), actions: [ CupertinoDialogAction( @@ -172,11 +183,12 @@ class _CustomDrawerState extends State { children: [ // En-tête avec sa propre SafeArea Material( - color: Theme.of(context).primaryColor.withOpacity(0.9), + color: Theme.of(context).primaryColor.withValues(alpha: 0.9), child: SafeArea( bottom: false, child: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), + padding: const EdgeInsets.only( + left: 16.0, right: 16.0, bottom: 16.0), child: GestureDetector( onTap: () { UrlUtils.launchURL('https://realt.co/marketplace/'); @@ -190,7 +202,7 @@ class _CustomDrawerState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(10), ), child: Image.asset( @@ -202,21 +214,25 @@ class _CustomDrawerState extends State { const SizedBox(width: 14), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( 'RealToken Asset Tracker', style: TextStyle( color: Colors.white, - fontSize: 18 + appState.getTextSizeOffset(), + fontSize: + 18 + appState.getTextSizeOffset(), fontWeight: FontWeight.w600, ), ), Text( S.of(context).appDescription, style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14 + appState.getTextSizeOffset(), + color: Colors.white + .withValues(alpha: 0.9), + fontSize: + 14 + appState.getTextSizeOffset(), ), ), ], @@ -224,7 +240,7 @@ class _CustomDrawerState extends State { ), ], ), - ], + ], ), ), ), @@ -243,7 +259,8 @@ class _CustomDrawerState extends State { bottom: 0, child: Container( decoration: BoxDecoration( - color: CupertinoColors.systemBackground.resolveFrom(context), + color: CupertinoColors.systemBackground + .resolveFrom(context), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -270,7 +287,8 @@ class _CustomDrawerState extends State { items: [ DrawerMenuFactory.createPageNavItem( context: context, - icon: CupertinoIcons.person_crop_circle_fill, + icon: CupertinoIcons + .person_crop_circle_fill, title: S.of(context).manageEvmAddresses, page: const ManageEvmAddressesPage(), ), @@ -301,7 +319,8 @@ class _CustomDrawerState extends State { ), DrawerMenuFactory.createPageNavItem( context: context, - icon: CupertinoIcons.arrow_clockwise_circle_fill, + icon: CupertinoIcons + .arrow_clockwise_circle_fill, title: S.of(context).recentChanges, page: const UpdatesPage(), iconColor: Colors.orange, @@ -347,7 +366,8 @@ class _CustomDrawerState extends State { ), DrawerMenuFactory.createPageNavItem( context: context, - icon: CupertinoIcons.chat_bubble_text_fill, + icon: CupertinoIcons + .chat_bubble_text_fill, title: S.of(context).support, page: const SupportPage(), iconColor: Colors.green, diff --git a/lib/structure/home_page.dart b/lib/structure/home_page.dart index 218826a..51624e5 100644 --- a/lib/structure/home_page.dart +++ b/lib/structure/home_page.dart @@ -5,7 +5,6 @@ import 'package:realtoken_asset_tracker/utils/currency_utils.dart'; import 'package:realtoken_asset_tracker/utils/ui_utils.dart'; import 'package:realtoken_asset_tracker/utils/performance_utils.dart'; import 'package:realtoken_asset_tracker/utils/cache_constants.dart'; -import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'dart:math'; import 'bottom_bar.dart'; @@ -31,7 +30,8 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { int _selectedIndex = 0; - final GlobalKey _walletIconKey = GlobalKey(); // Clé pour obtenir la position de l'icône + final GlobalKey _walletIconKey = + GlobalKey(); // Clé pour obtenir la position de l'icône List> portfolio = []; @@ -43,28 +43,31 @@ class _MyHomePageState extends State { final random = Random(); // Ajout de la condition de probabilité - if (random.nextInt(3) == 0) { // Génère 0, 1, ou 2. La condition est vraie pour 0 (1/3 des cas) + if (random.nextInt(3) == 0) { + // Génère 0, 1, ou 2. La condition est vraie pour 0 (1/3 des cas) final delaySeconds = 5 + random.nextInt(26); // 5 à 30 inclus - Future.delayed(Duration(seconds: delaySeconds), _showDonationPopupIfNeeded); + Future.delayed( + Duration(seconds: delaySeconds), _showDonationPopupIfNeeded); } } void _showDonationPopupIfNeeded() async { if (_donationPopupShown) return; if (!mounted) return; - + // Throttle pour éviter les appels répétitifs - if (!PerformanceUtils.throttle('donation_popup_check', const Duration(seconds: 30))) { + if (!PerformanceUtils.throttle( + 'donation_popup_check', const Duration(seconds: 30))) { return; } - + final appState = Provider.of(context, listen: false); if (!appState.shouldShowDonationPopup) return; _donationPopupShown = true; - + // Mettre à jour le timestamp de la dernière popup affichée await appState.updateLastDonationPopupTimestamp(); - + if (!mounted) return; showDialog( context: context, @@ -126,7 +129,8 @@ class _MyHomePageState extends State { final dataManager = Provider.of(context); final currencyUtils = Provider.of(context, listen: false); - final double walletTotal = dataManager.gnosisUsdcBalance + dataManager.gnosisXdaiBalance; + final double walletTotal = + dataManager.gnosisUsdcBalance + dataManager.gnosisXdaiBalance; return Scaffold( body: Stack( @@ -143,7 +147,9 @@ class _MyHomePageState extends State { filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( height: UIUtils.getAppBarHeight(context), - color: Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.3) : Colors.white.withOpacity(0.3), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.3), child: AppBar( forceMaterialTransparency: true, backgroundColor: Colors.transparent, @@ -151,7 +157,8 @@ class _MyHomePageState extends State { actions: [ // Icône portefeuille avec un Popup Menu IconButton( - key: _walletIconKey, // Associe la clé pour obtenir la position + key: + _walletIconKey, // Associe la clé pour obtenir la position icon: Icon( Icons.account_balance_wallet, size: 21 + appState.getTextSizeOffset(), @@ -165,17 +172,22 @@ class _MyHomePageState extends State { onTap: () => _showWalletPopup(context), borderRadius: BorderRadius.circular(4), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), + padding: const EdgeInsets.symmetric( + horizontal: 4.0, vertical: 2.0), child: Center( child: Text( currencyUtils.getFormattedAmount( currencyUtils.convert(walletTotal), currencyUtils.currencySymbol, - appState.showAmounts, // Utilisation de showAmounts + appState + .showAmounts, // Utilisation de showAmounts ), style: TextStyle( fontSize: 16 + appState.getTextSizeOffset(), - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), ), ), @@ -192,7 +204,9 @@ class _MyHomePageState extends State { ), IconButton( icon: Icon( - appState.showAmounts ? Icons.visibility : Icons.visibility_off, + appState.showAmounts + ? Icons.visibility + : Icons.visibility_off, size: 22 + appState.getTextSizeOffset(), color: Theme.of(context).textTheme.bodyMedium?.color, ), @@ -213,7 +227,9 @@ class _MyHomePageState extends State { filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( height: _getContainerHeight(context), - color: Theme.of(context).brightness == Brightness.dark ? Colors.black.withOpacity(0.3) : Colors.white.withOpacity(0.3), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.3), child: SafeArea( top: false, child: CustomBottomNavigationBar( @@ -239,7 +255,8 @@ class _MyHomePageState extends State { // Widget interne pour gérer le chargement asynchrone du montant du wallet class _DonationPopupAsyncLoader extends StatefulWidget { @override - State<_DonationPopupAsyncLoader> createState() => _DonationPopupAsyncLoaderState(); + State<_DonationPopupAsyncLoader> createState() => + _DonationPopupAsyncLoaderState(); } class _DonationPopupAsyncLoaderState extends State<_DonationPopupAsyncLoader> { @@ -254,7 +271,7 @@ class _DonationPopupAsyncLoaderState extends State<_DonationPopupAsyncLoader> { Future _fetchWalletAmount() async { const cacheKey = 'donation_wallet_amount'; - + // Vérifier le cache d'abord avec PerformanceUtils final cachedAmount = PerformanceUtils.getFromCache(cacheKey); if (cachedAmount != null) { @@ -268,22 +285,25 @@ class _DonationPopupAsyncLoaderState extends State<_DonationPopupAsyncLoader> { const walletAddress = DonationWidgets.donationAddress; try { // Appel direct à ApiService.fetchRmmBalances pour une seule adresse - final balances = await ApiService.fetchRmmBalancesForAddress(walletAddress); + final balances = + await ApiService.fetchRmmBalancesForAddress(walletAddress); if (balances.isNotEmpty) { final wallet = balances.first; debugPrint('🎁 Donation wallet balance: ${wallet.toString()}'); - + final double gnosisUsdc = wallet['gnosisUsdcBalance'] ?? 0.0; final double gnosisXdai = wallet['gnosisXdaiBalance'] ?? 0.0; final double usdcDeposit = wallet['usdcDepositBalance'] ?? 0.0; final double xdaiDeposit = wallet['xdaiDepositBalance'] ?? 0.0; - final double total = gnosisUsdc + gnosisXdai + usdcDeposit + xdaiDeposit; - + final double total = + gnosisUsdc + gnosisXdai + usdcDeposit + xdaiDeposit; + final String formattedAmount = total.toStringAsFixed(2); - + // Mettre à jour le cache avec PerformanceUtils - PerformanceUtils.setCache(cacheKey, formattedAmount, CacheConstants.donationAmountCache); - + PerformanceUtils.setCache( + cacheKey, formattedAmount, CacheConstants.donationAmountCache); + if (mounted) { setState(() { montant = formattedAmount; @@ -331,7 +351,7 @@ class _DonationPopupAsyncLoaderState extends State<_DonationPopupAsyncLoader> { right: 0, child: IconButton( icon: Icon( - Icons.close_rounded, + Icons.close_rounded, size: 28, color: Theme.of(context).iconTheme.color, ), diff --git a/lib/utils/cache_constants.dart b/lib/utils/cache_constants.dart index 7ea3446..aa92c11 100644 --- a/lib/utils/cache_constants.dart +++ b/lib/utils/cache_constants.dart @@ -7,7 +7,7 @@ class CacheConstants { static const Duration mediumCache = Duration(hours: 1); static const Duration longCache = Duration(hours: 6); static const Duration veryLongCache = Duration(hours: 24); - + // Caches spécifiques par type de données static const Duration balancesCache = Duration(minutes: 15); static const Duration rentDataCache = Duration(hours: 1); @@ -19,17 +19,17 @@ class CacheConstants { static const Duration transactionsCache = Duration(hours: 3); static const Duration whitelistCache = Duration(hours: 2); static const Duration detailedRentCache = Duration(hours: 2); - + // Caches UI static const Duration versionCache = Duration(hours: 6); static const Duration donationAmountCache = Duration(hours: 1); static const Duration apiStatusCache = Duration(minutes: 30); - + // Délais pour éviter les requêtes répétitives static const Duration rateLimitDelay = Duration(minutes: 5); static const Duration retryDelay = Duration(seconds: 30); - + // Limites de mémoire static const int maxCacheEntries = 1000; static const int maxImageCacheSize = 100 * 1024 * 1024; // 100MB -} \ No newline at end of file +} diff --git a/lib/utils/chart_options_utils.dart b/lib/utils/chart_options_utils.dart index 8cd6b9e..2dd2f6a 100644 --- a/lib/utils/chart_options_utils.dart +++ b/lib/utils/chart_options_utils.dart @@ -14,7 +14,7 @@ class ChartOptionsUtils { }) { final appState = Provider.of(context, listen: false); final primaryColor = Theme.of(context).primaryColor; - + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -38,7 +38,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.chart_bar, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -70,7 +70,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.chart_bar_alt_fill, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -98,7 +98,7 @@ class ChartOptionsUtils { ], ); } - + /// Construit le sélecteur de plage temporelle static Widget buildTimeRangeSelector({ required BuildContext context, @@ -109,7 +109,7 @@ class ChartOptionsUtils { final appState = Provider.of(context, listen: false); final primaryColor = Theme.of(context).primaryColor; final textColor = Theme.of(context).textTheme.bodyLarge?.color; - + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -136,7 +136,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -168,7 +168,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -200,7 +200,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -232,7 +232,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -262,7 +262,7 @@ class ChartOptionsUtils { ], ); } - + /// Construit le sélecteur de période d'affichage static Widget buildPeriodSelector({ required BuildContext context, @@ -273,7 +273,7 @@ class ChartOptionsUtils { final appState = Provider.of(context, listen: false); final primaryColor = Theme.of(context).primaryColor; final textColor = Theme.of(context).textTheme.bodyLarge?.color; - + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -300,7 +300,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar_today, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -332,7 +332,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar_badge_plus, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -364,7 +364,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar_circle, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -396,7 +396,7 @@ class ChartOptionsUtils { visualDensity: const VisualDensity(horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.calendar_circle_fill, - color: primaryColor.withOpacity(0.8), + color: primaryColor.withValues(alpha: 0.8), size: 22, ), title: Text( @@ -439,7 +439,7 @@ class ChartOptionsUtils { VoidCallback? onEditPressed, }) { final appState = Provider.of(context, listen: false); - + showModalBottomSheet( context: context, isScrollControlled: true, @@ -453,7 +453,7 @@ class ChartOptionsUtils { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -2), ), @@ -472,13 +472,13 @@ class ChartOptionsUtils { width: 36, height: 5, decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(3), ), ), ), ), - + Padding( padding: const EdgeInsets.symmetric( vertical: 24.0, @@ -495,12 +495,13 @@ class ChartOptionsUtils { onChartTypeChanged: onChartTypeChanged, onClose: () => Navigator.of(context).pop(), ), - + const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), child: Divider(height: 1, thickness: 0.5), ), - + // Sélecteur de plage de temps buildTimeRangeSelector( context: context, @@ -508,12 +509,13 @@ class ChartOptionsUtils { onTimeRangeChanged: onTimeRangeChanged, onClose: () => Navigator.of(context).pop(), ), - + const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), child: Divider(height: 1, thickness: 0.5), ), - + // Sélecteur de période buildPeriodSelector( context: context, @@ -521,20 +523,24 @@ class ChartOptionsUtils { onPeriodChanged: onPeriodChanged, onClose: () => Navigator.of(context).pop(), ), - + if (onEditPressed != null) ...[ const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), child: Divider(height: 1, thickness: 0.5), ), SizedBox( height: 44, child: ListTile( dense: true, - visualDensity: const VisualDensity(horizontal: 0, vertical: -2), + visualDensity: const VisualDensity( + horizontal: 0, vertical: -2), leading: Icon( CupertinoIcons.pencil, - color: Theme.of(context).primaryColor.withOpacity(0.8), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.8), size: 22, ), title: Text( @@ -543,7 +549,10 @@ class ChartOptionsUtils { fontWeight: FontWeight.w500, fontSize: 15 + appState.getTextSizeOffset(), letterSpacing: -0.4, - color: Theme.of(context).textTheme.bodyLarge?.color, + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color, ), ), onTap: () { @@ -563,4 +572,4 @@ class ChartOptionsUtils { }, ); } -} \ No newline at end of file +} diff --git a/lib/utils/chart_performance_utils.dart b/lib/utils/chart_performance_utils.dart index 6aacf5c..ad3eb65 100644 --- a/lib/utils/chart_performance_utils.dart +++ b/lib/utils/chart_performance_utils.dart @@ -4,141 +4,147 @@ import 'package:realtoken_asset_tracker/utils/performance_utils.dart'; /// Utilitaires pour optimiser les performances des graphiques class ChartPerformanceUtils { - /// Échantillonne intelligemment les données pour éviter les graphiques surchargés static List sampleData(List data, {int maxPoints = 100}) { if (data.length <= maxPoints) return data; - + final step = (data.length / maxPoints).ceil(); final sampled = []; - + // Toujours inclure le premier et dernier point sampled.add(data.first); - + for (int i = step; i < data.length - 1; i += step) { sampled.add(data[i]); } - + // Toujours inclure le dernier point if (data.length > 1) { sampled.add(data.last); } - + return sampled; } - + /// Cache les spots FlChart pour éviter les recalculs static List getCachedSpots(String key, List values) { const cacheDuration = Duration(minutes: 5); - + final cached = PerformanceUtils.getFromCache>(key); if (cached != null) return cached; - - final spots = values.asMap().entries + + final spots = values + .asMap() + .entries .map((entry) => FlSpot(entry.key.toDouble(), entry.value)) .toList(); - + PerformanceUtils.setCache(key, spots, cacheDuration); return spots; } - + /// Optimise les couleurs pour les graphiques avec cache static Color getCachedColor(String key, Color Function() colorGenerator) { const cacheDuration = Duration(hours: 1); - + final cached = PerformanceUtils.getFromCache(key); if (cached != null) return cached; - + final color = colorGenerator(); PerformanceUtils.setCache(key, color, cacheDuration); return color; } - + /// Calcule efficacement les min/max pour les axes static MapEntry getMinMaxWithPadding( List values, { double paddingPercent = 0.1, }) { if (values.isEmpty) return const MapEntry(0.0, 1.0); - + double min = values.first; double max = values.first; - + for (final value in values) { if (value < min) min = value; if (value > max) max = value; } - + final range = max - min; final padding = range * paddingPercent; - + return MapEntry( (min - padding).clamp(0.0, double.infinity), max + padding, ); } - + /// Simplifie les paths complexes pour de meilleures performances - static List simplifyPath(List spots, {double tolerance = 1.0}) { + static List simplifyPath(List spots, + {double tolerance = 1.0}) { if (spots.length <= 3) return spots; - + final simplified = [spots.first]; - + for (int i = 1; i < spots.length - 1; i++) { final prev = simplified.last; final current = spots[i]; final next = spots[i + 1]; - + // Calculer la distance perpendiculaire du point actuel à la ligne prev-next final distance = _perpendicularDistance(current, prev, next); - + if (distance > tolerance) { simplified.add(current); } } - + simplified.add(spots.last); return simplified; } - + /// Calcule la distance perpendiculaire d'un point à une ligne - static double _perpendicularDistance(FlSpot point, FlSpot lineStart, FlSpot lineEnd) { + static double _perpendicularDistance( + FlSpot point, FlSpot lineStart, FlSpot lineEnd) { final dx = lineEnd.x - lineStart.x; final dy = lineEnd.y - lineStart.y; - + if (dx == 0 && dy == 0) { return (point.x - lineStart.x).abs() + (point.y - lineStart.y).abs(); } - - final t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy); + + final t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / + (dx * dx + dy * dy); final projection = FlSpot( lineStart.x + t * dx, lineStart.y + t * dy, ); - + final distX = point.x - projection.x; final distY = point.y - projection.y; - + return (distX * distX + distY * distY).abs(); } - + /// Optimise l'affichage des tooltips avec debouncing static void showTooltipDebounced(String key, VoidCallback showTooltip) { - PerformanceUtils.debounce(key, const Duration(milliseconds: 300), showTooltip); + PerformanceUtils.debounce( + key, const Duration(milliseconds: 300), showTooltip); } - + /// Batch les mises à jour des graphiques pour éviter les rebuilds multiples static final Map> _pendingUpdates = {}; - + static void batchChartUpdate(String chartKey, VoidCallback update) { _pendingUpdates.putIfAbsent(chartKey, () => []).add(update); - - PerformanceUtils.debounce('chart_batch_$chartKey', + + PerformanceUtils.debounce( + 'chart_batch_$chartKey', const Duration(milliseconds: 16), // ~60fps () => _flushChartUpdates(chartKey), ); } - + static void _flushChartUpdates(String chartKey) { final updates = _pendingUpdates.remove(chartKey); if (updates != null) { @@ -147,9 +153,9 @@ class ChartPerformanceUtils { } } } - + /// Méthode pour nettoyer les ressources static void dispose() { _pendingUpdates.clear(); } -} \ No newline at end of file +} diff --git a/lib/utils/chart_utils.dart b/lib/utils/chart_utils.dart index b24162f..bbf7734 100644 --- a/lib/utils/chart_utils.dart +++ b/lib/utils/chart_utils.dart @@ -19,7 +19,9 @@ class ChartUtils { height: 28, margin: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark ? Colors.black12 : Colors.black.withOpacity(0.05), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black12 + : Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(18), ), child: Padding( @@ -27,17 +29,22 @@ class ChartUtils { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildPeriodButton(S.of(context).day, context, selectedPeriod, onPeriodChanged), - _buildPeriodButton(S.of(context).week, context, selectedPeriod, onPeriodChanged), - _buildPeriodButton(S.of(context).month, context, selectedPeriod, onPeriodChanged), - _buildPeriodButton(S.of(context).year, context, selectedPeriod, onPeriodChanged), + _buildPeriodButton( + S.of(context).day, context, selectedPeriod, onPeriodChanged), + _buildPeriodButton( + S.of(context).week, context, selectedPeriod, onPeriodChanged), + _buildPeriodButton( + S.of(context).month, context, selectedPeriod, onPeriodChanged), + _buildPeriodButton( + S.of(context).year, context, selectedPeriod, onPeriodChanged), ], ), ), ); } - static Widget _buildPeriodButton(String period, BuildContext context, String selectedPeriod, Function(String) onPeriodChanged) { + static Widget _buildPeriodButton(String period, BuildContext context, + String selectedPeriod, Function(String) onPeriodChanged) { final isSelected = selectedPeriod == period; final appState = Provider.of(context); @@ -46,7 +53,8 @@ class ChartUtils { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( - color: isSelected ? Theme.of(context).primaryColor : Colors.transparent, + color: + isSelected ? Theme.of(context).primaryColor : Colors.transparent, borderRadius: BorderRadius.circular(18), ), child: Text( @@ -66,7 +74,7 @@ class ChartUtils { if (dataLength <= maxBarsToDisplay) { return 1; // Pas de step si moins que le maximum } - + // Calcule le pas minimum pour ne pas dépasser maxBarsToDisplay return (dataLength / maxBarsToDisplay).ceil(); } @@ -74,27 +82,27 @@ class ChartUtils { /// Applique un step aux données pour limiter le nombre de points affichés static List applyStepToData(List spots) { if (spots.isEmpty) return spots; - + final step = calculateStep(spots.length); if (step == 1) return spots; // Pas de changement nécessaire - + List filteredSpots = []; - + // Toujours inclure le premier et le dernier point if (spots.isNotEmpty) { filteredSpots.add(spots.first); } - + // Ajouter les points intermédiaires selon le step for (int i = step; i < spots.length - 1; i += step) { filteredSpots.add(spots[i]); } - + // Ajouter le dernier point s'il n'est pas déjà inclus if (spots.length > 1 && spots.last != filteredSpots.last) { filteredSpots.add(spots.last); } - + // Recalculer les indices x pour qu'ils soient séquentiels return filteredSpots.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.y); @@ -102,13 +110,14 @@ class ChartUtils { } static List buildHistoryChartData( - BuildContext context, - List history, - String selectedPeriod, - double Function(T) getValue, - DateTime Function(T) getTimestamp, - {bool applyStep = true, String aggregate = "average"} // Ajout du paramètre aggregate - ) { + BuildContext context, + List history, + String selectedPeriod, + double Function(T) getValue, + DateTime Function(T) getTimestamp, + {bool applyStep = true, + String aggregate = "average"} // Ajout du paramètre aggregate + ) { Map> groupedData = {}; for (var record in history) { @@ -118,7 +127,8 @@ class ChartUtils { if (selectedPeriod == S.of(context).day) { periodKey = DateFormat('yyyy/MM/dd').format(date); } else if (selectedPeriod == S.of(context).week) { - periodKey = "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; + periodKey = + "${date.year}-S${CustomDateUtils.weekNumber(date).toString().padLeft(2, '0')}"; } else if (selectedPeriod == S.of(context).month) { periodKey = DateFormat('yyyy/MM').format(date); } else { @@ -153,35 +163,35 @@ class ChartUtils { /// Applique un step aux dates pour limiter le nombre d'étiquettes affichées static List applyStepToLabels(List labels) { if (labels.isEmpty) return labels; - + final step = calculateStep(labels.length); if (step == 1) return labels; // Pas de changement nécessaire - + List filteredLabels = []; - + // Toujours inclure la première et la dernière étiquette if (labels.isNotEmpty) { filteredLabels.add(labels.first); } - + // Ajouter les étiquettes intermédiaires selon le step for (int i = step; i < labels.length - 1; i += step) { filteredLabels.add(labels[i]); } - + // Ajouter la dernière étiquette si elle n'est pas déjà incluse if (labels.length > 1 && !filteredLabels.contains(labels.last)) { filteredLabels.add(labels.last); } - + // Compléter avec des étiquettes vides pour maintenir l'alignement final fullLength = filteredLabels.length; List completeLabels = List.filled(fullLength, ""); - + for (int i = 0; i < filteredLabels.length; i++) { completeLabels[i] = filteredLabels[i]; } - + return completeLabels; } @@ -199,7 +209,9 @@ class ChartUtils { onTap: onTap, child: Container( decoration: BoxDecoration( - color: isSelected ? Theme.of(context).primaryColor : Theme.of(context).secondaryHeaderColor, + color: isSelected + ? Theme.of(context).primaryColor + : Theme.of(context).secondaryHeaderColor, borderRadius: BorderRadius.horizontal( left: isFirst ? const Radius.circular(8) : Radius.zero, right: isLast ? const Radius.circular(8) : Radius.zero, @@ -211,7 +223,9 @@ class ChartUtils { period, style: TextStyle( fontSize: 14 + appState.getTextSizeOffset(), - color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyLarge?.color, + color: isSelected + ? Colors.white + : Theme.of(context).textTheme.bodyLarge?.color, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), diff --git a/lib/utils/contracts_constants.dart b/lib/utils/contracts_constants.dart index eeb942a..a8946b0 100644 --- a/lib/utils/contracts_constants.dart +++ b/lib/utils/contracts_constants.dart @@ -1,35 +1,43 @@ class ContractsConstants { // === ADRESSES DE CONTRATS RMM === - static const String usdcDepositContract = '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1'; - static const String usdcBorrowContract = '0x69c731ae5f5356a779f44c355abb685d84e5e9e6'; - static const String xdaiDepositContract = '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b'; - static const String xdaiBorrowContract = '0x9908801df7902675c3fedd6fea0294d18d5d5d34'; - + static const String usdcDepositContract = + '0xed56f76e9cbc6a64b821e9c016eafbd3db5436d1'; + static const String usdcBorrowContract = + '0x69c731ae5f5356a779f44c355abb685d84e5e9e6'; + static const String xdaiDepositContract = + '0x0ca4f5554dd9da6217d62d8df2816c82bba4157b'; + static const String xdaiBorrowContract = + '0x9908801df7902675c3fedd6fea0294d18d5d5d34'; + // === CONTRATS GNOSIS === - static const String gnosisUsdcContract = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83'; - static const String gnosisRegContract = '0x0aa1e96d2a46ec6beb2923de1e61addf5f5f1dce'; - static const String gnosisVaultRegContract = '0xe1877d33471e37fe0f62d20e60c469eff83fb4a0'; - + static const String gnosisUsdcContract = + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83'; + static const String gnosisRegContract = + '0x0aa1e96d2a46ec6beb2923de1e61addf5f5f1dce'; + static const String gnosisVaultRegContract = + '0xe1877d33471e37fe0f62d20e60c469eff83fb4a0'; + // === RPC ENDPOINTS === static const String gnosisRpcUrl = 'https://rpc.gnosischain.com'; - + // === SELECTEURS DE FONCTION === static const String balanceOfSelector = '0x70a08231'; static const String vaultBalanceSelector = '0xf262a083'; - + // === DÉCIMALES === static const int usdcDecimals = 6; static const int xdaiDecimals = 18; static const int regDecimals = 18; - + // === DIVISEURS POUR CONVERSION === static final BigInt usdcDivisor = BigInt.from(1e6); static final BigInt xdaiDivisor = BigInt.from(1e18); static final BigInt regDivisor = BigInt.from(1e18); - + // === ADRESSE DE DONATION === - static const String donationWalletAddress = '0xdc30b07aebaef3f15544a3801c6cb0f35f0118fc'; - + static const String donationWalletAddress = + '0xdc30b07aebaef3f15544a3801c6cb0f35f0118fc'; + // === DURÉES DE CACHE SPÉCIFIQUES === static const Duration rmmBalancesCacheDuration = Duration(minutes: 15); static const Duration rentDataCacheDuration = Duration(hours: 1); @@ -38,44 +46,42 @@ class ContractsConstants { static const Duration tokenVolumesCacheDuration = Duration(hours: 4); static const Duration transactionsCacheDuration = Duration(hours: 3); static const Duration currenciesCacheDuration = Duration(hours: 1); - + // === LIMITES ET SEUILS === static const Duration version429Delay = Duration(minutes: 5); static const Duration oneHourInMillis = Duration(hours: 1); - + // === MÉTHODES UTILITAIRES === - + /// Construit la data pour un appel eth_call balanceOf static String buildBalanceOfData(String address) { - final String paddedAddress = address.toLowerCase() - .replaceFirst('0x', '') - .padLeft(64, '0'); + final String paddedAddress = + address.toLowerCase().replaceFirst('0x', '').padLeft(64, '0'); return '0x$balanceOfSelector$paddedAddress'; } - + /// Construit la data pour un appel vault balance static String buildVaultBalanceData(String address) { - final String paddedAddress = address.toLowerCase() - .replaceFirst('0x', '') - .padLeft(64, '0'); + final String paddedAddress = + address.toLowerCase().replaceFirst('0x', '').padLeft(64, '0'); return '0x$vaultBalanceSelector$paddedAddress'; } - + /// Convertit une balance USDC en double static double convertUsdcBalance(BigInt balance) { return balance / usdcDivisor; } - + /// Convertit une balance XDAI/REG en double static double convertXdaiBalance(BigInt balance) { return balance / xdaiDivisor; } - + /// Convertit une balance REG en double static double convertRegBalance(BigInt balance) { return balance / regDivisor; } - + /// Retourne tous les contrats RMM pour les requêtes en lot static List getAllRmmContracts() { return [ @@ -87,7 +93,7 @@ class ContractsConstants { gnosisRegContract, ]; } - + /// Map des contrats avec leurs infos (nom, décimales, diviseur) static Map> getContractInfo() { return { @@ -129,4 +135,4 @@ class ContractsConstants { }, }; } -} \ No newline at end of file +} diff --git a/lib/utils/currency_utils.dart b/lib/utils/currency_utils.dart index a461556..7ad56df 100644 --- a/lib/utils/currency_utils.dart +++ b/lib/utils/currency_utils.dart @@ -27,21 +27,26 @@ class CurrencyProvider extends ChangeNotifier { notifyListeners(); // ✅ Force l'UI à se mettre à jour } - Future updateConversionRate(String currency, Map currencies) async { + Future updateConversionRate( + String currency, Map currencies) async { final prefs = await SharedPreferences.getInstance(); - await prefs.setString('selectedCurrency', currency); // ✅ Sauvegarde la nouvelle devise + await prefs.setString( + 'selectedCurrency', currency); // ✅ Sauvegarde la nouvelle devise _selectedCurrency = currency; if (_selectedCurrency == "usd") { _conversionRate = 1.0; } else if (currencies.containsKey(_selectedCurrency)) { - _conversionRate = currencies[_selectedCurrency] is double ? currencies[_selectedCurrency] : 1.0; + _conversionRate = currencies[_selectedCurrency] is double + ? currencies[_selectedCurrency] + : 1.0; } else { _conversionRate = 1.0; } - _currencySymbol = Parameters.currencySymbols[_selectedCurrency] ?? _selectedCurrency.toUpperCase(); + _currencySymbol = Parameters.currencySymbols[_selectedCurrency] ?? + _selectedCurrency.toUpperCase(); notifyListeners(); // 🔥 Notifie l'UI qu'un changement a eu lieu } @@ -62,10 +67,14 @@ class CurrencyProvider extends ChangeNotifier { // Méthode pour formater ou masquer les montants en série de **** String getFormattedAmount(double value, String symbol, bool showAmount) { if (showAmount) { - return formatCurrency(value, symbol); // Affiche le montant formaté si visible + return formatCurrency( + value, symbol); // Affiche le montant formaté si visible } else { - String formattedValue = formatCurrency(value, symbol); // Format le montant normalement - return '*' * formattedValue.length; // Retourne une série d'astérisques de la même longueur + String formattedValue = + formatCurrency(value, symbol); // Format le montant normalement + return '*' * + formattedValue + .length; // Retourne une série d'astérisques de la même longueur } } diff --git a/lib/utils/date_utils.dart b/lib/utils/date_utils.dart index 4981d73..82cb178 100644 --- a/lib/utils/date_utils.dart +++ b/lib/utils/date_utils.dart @@ -31,7 +31,8 @@ class CustomDateUtils { DateTime localDate = parsedDate.toLocal(); // Formate la date avec l'heure dans un format lisible: 1 Dec 2024 14:30:45 - String formattedDate = DateFormat('d MMM yyyy HH:mm:ss').format(localDate); + String formattedDate = + DateFormat('d MMM yyyy HH:mm:ss').format(localDate); return formattedDate; } catch (e) { diff --git a/lib/utils/location_utils.dart b/lib/utils/location_utils.dart index 6a13a9d..bf8e37f 100644 --- a/lib/utils/location_utils.dart +++ b/lib/utils/location_utils.dart @@ -3,7 +3,6 @@ import 'package:realtoken_asset_tracker/generated/l10n.dart'; /// Utilitaires pour le parsing et la gestion des données de localisation /// Factorisation du parsing de fullName répétitif dans l'application class LocationUtils { - /// Extrait la ville depuis un fullName formaté static String extractCity(String fullName) { List parts = fullName.split(','); @@ -13,7 +12,9 @@ class LocationUtils { /// Extrait la région depuis un fullName formaté static String extractRegion(String fullName) { List parts = fullName.split(','); - return parts.length >= 3 ? parts[2].trim().substring(0, 2) : S.current.unknown.toLowerCase(); + return parts.length >= 3 + ? parts[2].trim().substring(0, 2) + : S.current.unknown.toLowerCase(); } /// Extrait le pays depuis un fullName formaté @@ -27,7 +28,9 @@ class LocationUtils { final parts = fullName.split(','); return { 'city': parts.length >= 2 ? parts[1].trim() : S.current.unknownCity, - 'regionCode': parts.length >= 3 ? parts[2].trim().substring(0, 2) : S.current.unknown.toLowerCase(), + 'regionCode': parts.length >= 3 + ? parts[2].trim().substring(0, 2) + : S.current.unknown.toLowerCase(), 'country': parts.length == 4 ? parts[3].trim() : "USA", }; } @@ -35,7 +38,8 @@ class LocationUtils { /// Formate les pieds carrés en mètres carrés ou garde les pieds carrés static String formatSquareFeet(double sqft, bool convertToSquareMeters) { if (convertToSquareMeters) { - double squareMeters = sqft * 0.092903; // Conversion des pieds carrés en m² + double squareMeters = + sqft * 0.092903; // Conversion des pieds carrés en m² return '${squareMeters.toStringAsFixed(2)} m²'; } else { return '${sqft.toStringAsFixed(2)} sqft'; diff --git a/lib/utils/parameters.dart b/lib/utils/parameters.dart index e880f2b..6a9aba1 100644 --- a/lib/utils/parameters.dart +++ b/lib/utils/parameters.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; @@ -15,24 +14,37 @@ class Parameters { static const String mainApiUrl = 'https://api.vfhome.fr'; static const String realTokensUrl = 'https://api.pitsbi.io/api'; - static const String rentTrackerUrl = 'https://ehpst.duckdns.org/realt_rent_tracker/api'; - static const String coingeckoUrl = 'https://api.coingecko.com/api/v3/coins/xdai'; + static const String rentTrackerUrl = + 'https://ehpst.duckdns.org/realt_rent_tracker/api'; + static const String coingeckoUrl = + 'https://api.coingecko.com/api/v3/coins/xdai'; - static const List stables = ["0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", "0xddafbb505ad214d7b80b1f830fccc89b60fb7a83", "0x7349c9eaa538e118725a6130e0f8341509b9f8a0"]; + static const List stables = [ + "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + "0xddafbb505ad214d7b80b1f830fccc89b60fb7a83", + "0x7349c9eaa538e118725a6130e0f8341509b9f8a0" + ]; static String rwaTokenAddress = '0x0675e8f4a52ea6c845cb6427af03616a2af42170'; // 📌 Autres paramètres inchangés static const Duration apiCacheDuration = Duration(minutes: 1); static bool convertToSquareMeters = false; static String selectedCurrency = 'usd'; static List languages = ['en', 'fr', 'es', "zh", "it", "pt", "ru"]; - static List textSizeOptions = ['verySmall', 'small', 'normal', 'big', 'veryBig']; + static List textSizeOptions = [ + 'verySmall', + 'small', + 'normal', + 'big', + 'veryBig' + ]; // Paramètres pour le portfolio - transformés en getters static bool get showTotalInvested => _appState?.showTotalInvested ?? false; static bool get showNetTotal => _appState?.showNetTotal ?? true; static double get manualAdjustment => _appState?.manualAdjustment ?? 0.0; static bool get showYamProjection => _appState?.showYamProjection ?? true; - static double get initialInvestmentAdjustment => _appState?.initialInvestmentAdjustment ?? 0.0; + static double get initialInvestmentAdjustment => + _appState?.initialInvestmentAdjustment ?? 0.0; // Méthodes pour mettre à jour les paramètres static void setShowTotalInvested(bool value) { @@ -135,7 +147,15 @@ class Parameters { 'WY': 'Wyoming' }; - static final Map currencySymbols = {'usd': '\$', 'eur': '€', 'gbp': '£', 'jpy': '¥', 'inr': '₹', 'btc': '₿', 'eth': 'Ξ'}; + static final Map currencySymbols = { + 'usd': '\$', + 'eur': '€', + 'gbp': '£', + 'jpy': '¥', + 'inr': '₹', + 'btc': '₿', + 'eth': 'Ξ' + }; // Mapping des régions spéciales static String getRegionDisplayName(String region) { diff --git a/lib/utils/performance_utils.dart b/lib/utils/performance_utils.dart index 296fa75..d1d44e6 100644 --- a/lib/utils/performance_utils.dart +++ b/lib/utils/performance_utils.dart @@ -3,33 +3,32 @@ import 'package:flutter/material.dart'; /// Utilitaires pour améliorer les performances de l'application class PerformanceUtils { - /// Debouncer pour éviter les appels répétitifs static final Map _debouncers = {}; - + /// Debounce une fonction avec une clé unique static void debounce(String key, Duration delay, VoidCallback action) { _debouncers[key]?.cancel(); _debouncers[key] = Timer(delay, action); } - + /// Throttle une fonction pour limiter sa fréquence d'exécution static final Map _lastExecutions = {}; - + static bool throttle(String key, Duration minInterval) { final now = DateTime.now(); final lastExecution = _lastExecutions[key]; - + if (lastExecution == null || now.difference(lastExecution) >= minInterval) { _lastExecutions[key] = now; return true; } return false; } - + /// Cache en mémoire simple avec expiration static final Map _memoryCache = {}; - + static T? getFromCache(String key) { final entry = _memoryCache[key]; if (entry != null && !entry.isExpired) { @@ -38,74 +37,75 @@ class PerformanceUtils { _memoryCache.remove(key); return null; } - + static void setCache(String key, T value, Duration duration) { _memoryCache[key] = _CacheEntry(value, DateTime.now().add(duration)); - + // Nettoyage du cache si trop d'entrées if (_memoryCache.length > 1000) { _cleanupCache(); } } - + static void _cleanupCache() { final now = DateTime.now(); _memoryCache.removeWhere((key, entry) => entry.expiry.isBefore(now)); } - + /// Optimise les BigInt operations courantes static final Map _bigIntCache = {}; - + static BigInt getBigIntPower(int base, int exponent) { final key = '${base}_$exponent'; return _bigIntCache.putIfAbsent(key, () => BigInt.from(base).pow(exponent)); } - + /// Constantes BigInt couramment utilisées pour éviter les recalculs static final BigInt oneE6 = BigInt.from(1000000); static final BigInt oneE18 = BigInt.from(10).pow(18); - + /// Conversion optimisée de BigInt vers double static double bigIntToDouble(BigInt value, int decimals) { final divisor = decimals == 6 ? oneE6 : oneE18; return value / divisor; } - + /// Batch des opérations SharedPreferences static final Map _pendingPrefs = {}; static Timer? _prefsTimer; - + static void batchSetPreference(String key, dynamic value) { _pendingPrefs[key] = value; - + _prefsTimer?.cancel(); _prefsTimer = Timer(const Duration(milliseconds: 100), _flushPreferences); } - + static Future _flushPreferences() async { if (_pendingPrefs.isEmpty) return; - + // Cette fonction devrait être appelée avec SharedPreferences.getInstance() // et batching des set operations final prefs = _pendingPrefs.entries.toList(); _pendingPrefs.clear(); - + // Notification pour que le code appelant puisse traiter le batch debugPrint('⚡ Batch preferences: ${prefs.length} items to flush'); } - + /// Widget builder optimisé avec cache - static Widget buildCachedWidget(String key, Duration cacheDuration, Widget Function() builder) { + static Widget buildCachedWidget( + String key, Duration cacheDuration, Widget Function() builder) { final cached = getFromCache(key); if (cached != null) { return cached; } - + final widget = builder(); setCache(key, widget, cacheDuration); return widget; } - + /// Nettoyage des ressources static void dispose() { for (final timer in _debouncers.values) { @@ -123,8 +123,8 @@ class PerformanceUtils { class _CacheEntry { final dynamic value; final DateTime expiry; - + _CacheEntry(this.value, this.expiry); - + bool get isExpired => DateTime.now().isAfter(expiry); -} \ No newline at end of file +} diff --git a/lib/utils/preference_keys.dart b/lib/utils/preference_keys.dart index cef0829..ee7ece9 100644 --- a/lib/utils/preference_keys.dart +++ b/lib/utils/preference_keys.dart @@ -1,128 +1,128 @@ /// Constantes pour toutes les clés utilisées dans SharedPreferences /// Centralisation pour éviter les erreurs de typo et faciliter la maintenance class PreferenceKeys { - // === PARAMÈTRES DE THÈME === static const String isDarkTheme = 'isDarkTheme'; static const String themeMode = 'themeMode'; static const String primaryColor = 'primaryColor'; - + // === PARAMÈTRES D'AFFICHAGE === static const String textSize = 'textSize'; static const String language = 'language'; static const String showAmounts = 'showAmounts'; - + // === PARAMÈTRES DU PORTFOLIO === static const String showTotalInvested = 'showTotalInvested'; static const String showNetTotal = 'showNetTotal'; static const String manualAdjustment = 'manualAdjustment'; static const String showYamProjection = 'showYamProjection'; - static const String initialInvestmentAdjustment = 'initialInvestmentAdjustment'; - + static const String initialInvestmentAdjustment = + 'initialInvestmentAdjustment'; + // === ADRESSES ET WALLETS === static const String evmAddresses = 'evmAddresses'; - + // === STATISTIQUES D'UTILISATION === static const String appOpenCount = 'appOpenCount'; static const String lastDonationPopupTimestamp = 'lastDonationPopupTimestamp'; - + // === AUTHENTIFICATION BIOMÉTRIQUE === static const String biometricEnabled = 'biometricEnabled'; static const String biometricType = 'biometricType'; - + // === CACHE ET PRÉFÉRENCES API === static const String lastApiUpdate = 'lastApiUpdate'; static const String cacheTimeout = 'cacheTimeout'; - + // === NOTIFICATIONS === static const String notificationsEnabled = 'notificationsEnabled'; static const String notificationTime = 'notificationTime'; static const String hasRefusedNotifications = 'hasRefusedNotifications'; static const String hasAskedNotifications = 'hasAskedNotifications'; - + // === PARAMÈTRES AVANCÉS === static const String debugMode = 'debugMode'; static const String analyticsEnabled = 'analyticsEnabled'; static const String crashReportsEnabled = 'crashReportsEnabled'; - + // === PRÉFÉRENCES DE DEVISE === static const String selectedCurrency = 'selectedCurrency'; static const String currencyUpdateFrequency = 'currencyUpdateFrequency'; - + // === PRÉFÉRENCES D'EXPORT === static const String lastExportPath = 'lastExportPath'; static const String exportFormat = 'exportFormat'; - + // === MÉTHODES UTILITAIRES === - + /// Retourne toutes les clés de thème static List get themeKeys => [ - isDarkTheme, - themeMode, - primaryColor, - ]; - + isDarkTheme, + themeMode, + primaryColor, + ]; + /// Retourne toutes les clés d'affichage static List get displayKeys => [ - textSize, - language, - showAmounts, - ]; - + textSize, + language, + showAmounts, + ]; + /// Retourne toutes les clés de portfolio static List get portfolioKeys => [ - showTotalInvested, - showNetTotal, - manualAdjustment, - showYamProjection, - initialInvestmentAdjustment, - ]; - + showTotalInvested, + showNetTotal, + manualAdjustment, + showYamProjection, + initialInvestmentAdjustment, + ]; + /// Retourne toutes les clés de sécurité static List get securityKeys => [ - biometricEnabled, - biometricType, - ]; - + biometricEnabled, + biometricType, + ]; + /// Retourne toutes les clés de données utilisateur static List get userDataKeys => [ - evmAddresses, - selectedCurrency, - ]; - + evmAddresses, + selectedCurrency, + ]; + /// Retourne toutes les clés critiques (ne pas supprimer lors d'un reset) static List get criticalKeys => [ - evmAddresses, - biometricEnabled, - selectedCurrency, - ]; - + evmAddresses, + biometricEnabled, + selectedCurrency, + ]; + /// Retourne toutes les clés existantes static List get allKeys => [ - ...themeKeys, - ...displayKeys, - ...portfolioKeys, - ...securityKeys, - ...userDataKeys, - appOpenCount, - lastDonationPopupTimestamp, - lastApiUpdate, - cacheTimeout, - notificationsEnabled, - notificationTime, - debugMode, - analyticsEnabled, - crashReportsEnabled, - currencyUpdateFrequency, - lastExportPath, - exportFormat, - ]; - + ...themeKeys, + ...displayKeys, + ...portfolioKeys, + ...securityKeys, + ...userDataKeys, + appOpenCount, + lastDonationPopupTimestamp, + lastApiUpdate, + cacheTimeout, + notificationsEnabled, + notificationTime, + debugMode, + analyticsEnabled, + crashReportsEnabled, + currencyUpdateFrequency, + lastExportPath, + exportFormat, + ]; + /// Vérifie si une clé est valide static bool isValidKey(String key) { return allKeys.contains(key); } - + /// Retourne la catégorie d'une clé static String getKeyCategory(String key) { if (themeKeys.contains(key)) return 'Thème'; @@ -132,4 +132,4 @@ class PreferenceKeys { if (userDataKeys.contains(key)) return 'Données utilisateur'; return 'Autres'; } -} \ No newline at end of file +} diff --git a/lib/utils/shimmer_utils.dart b/lib/utils/shimmer_utils.dart index 78efa9a..24485b5 100644 --- a/lib/utils/shimmer_utils.dart +++ b/lib/utils/shimmer_utils.dart @@ -14,23 +14,23 @@ class ShimmerUtils { children: [ // Le widget original sans modification child, - + // L'effet shimmer par-dessus avec un mode de fusion qui préserve les couleurs Positioned.fill( child: Shimmer.fromColors( baseColor: Colors.transparent, - highlightColor: color != null - ? color.withOpacity(0.6) - : Colors.white.withOpacity(0.7), + highlightColor: color != null + ? color.withValues(alpha: 0.6) + : Colors.white.withValues(alpha: 0.7), period: duration, child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, - color != null - ? color.withOpacity(0.5) - : Colors.white.withOpacity(0.6), + color != null + ? color.withValues(alpha: 0.5) + : Colors.white.withValues(alpha: 0.6), Colors.transparent, ], stops: const [0.0, 0.5, 1.0], @@ -44,7 +44,7 @@ class ShimmerUtils { ], ); } - + /// Crée un shimmer standard avec des couleurs personnalisées static Widget standardShimmer({ required Widget child, @@ -59,4 +59,4 @@ class ShimmerUtils { child: child, ); } -} \ No newline at end of file +} diff --git a/lib/utils/style_constants.dart b/lib/utils/style_constants.dart index c725f52..811185c 100644 --- a/lib/utils/style_constants.dart +++ b/lib/utils/style_constants.dart @@ -34,98 +34,128 @@ class StyleConstants { // Ombres standardisées static List get cardShadow => [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ]; + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ]; static List get subtleShadow => [ - BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 4, - offset: const Offset(0, 1), - ), - ]; + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ]; static List get modalShadow => [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - spreadRadius: 0, - ), - ]; + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + spreadRadius: 0, + ), + ]; // Décoration de carte standardisée static BoxDecoration cardDecoration(BuildContext context) => BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(cardBorderRadius), - boxShadow: cardShadow, - ); + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(cardBorderRadius), + boxShadow: cardShadow, + ); // Décoration de bouton standardisée - static BoxDecoration buttonDecoration(BuildContext context, {Color? backgroundColor}) => BoxDecoration( - color: backgroundColor ?? Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(buttonBorderRadius), - boxShadow: subtleShadow, - ); + static BoxDecoration buttonDecoration(BuildContext context, + {Color? backgroundColor}) => + BoxDecoration( + color: backgroundColor ?? Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + boxShadow: subtleShadow, + ); // Décoration de modal standardisée static BoxDecoration modalDecoration(BuildContext context) => BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.9) - : Colors.white.withOpacity(0.9), - borderRadius: const BorderRadius.vertical(top: Radius.circular(modalBorderRadius)), - boxShadow: modalShadow, - ); + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.9) + : Colors.white.withValues(alpha: 0.9), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(modalBorderRadius)), + boxShadow: modalShadow, + ); // Styles de texte avec offset - static TextStyle titleStyle(BuildContext context, double textSizeOffset) => TextStyle( - fontSize: 18 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.bodyLarge?.color, - ); - - static TextStyle subtitleStyle(BuildContext context, double textSizeOffset) => TextStyle( - fontSize: 16 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, - fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.bodyLarge?.color, - ); - - static TextStyle bodyStyle(BuildContext context, double textSizeOffset) => TextStyle( - fontSize: 14 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, - color: Theme.of(context).textTheme.bodyLarge?.color, - ); - - static TextStyle captionStyle(BuildContext context, double textSizeOffset) => TextStyle( - fontSize: 12 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), - ); - - static TextStyle smallStyle(BuildContext context, double textSizeOffset) => TextStyle( - fontSize: 10 + Provider.of(context, listen: false).getTextSizeOffset() + textSizeOffset, - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7), - ); + static TextStyle titleStyle(BuildContext context, double textSizeOffset) => + TextStyle( + fontSize: 18 + + Provider.of(context, listen: false).getTextSizeOffset() + + textSizeOffset, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyLarge?.color, + ); + + static TextStyle subtitleStyle(BuildContext context, double textSizeOffset) => + TextStyle( + fontSize: 16 + + Provider.of(context, listen: false).getTextSizeOffset() + + textSizeOffset, + fontWeight: FontWeight.w600, + color: Theme.of(context).textTheme.bodyLarge?.color, + ); + + static TextStyle bodyStyle(BuildContext context, double textSizeOffset) => + TextStyle( + fontSize: 14 + + Provider.of(context, listen: false).getTextSizeOffset() + + textSizeOffset, + color: Theme.of(context).textTheme.bodyLarge?.color, + ); + + static TextStyle captionStyle(BuildContext context, double textSizeOffset) => + TextStyle( + fontSize: 12 + + Provider.of(context, listen: false).getTextSizeOffset() + + textSizeOffset, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), + ); + + static TextStyle smallStyle(BuildContext context, double textSizeOffset) => + TextStyle( + fontSize: 10 + + Provider.of(context, listen: false).getTextSizeOffset() + + textSizeOffset, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7), + ); // Decorations spécialisées - static BoxDecoration primaryColorDecoration(BuildContext context, {double opacity = 0.1}) => BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(opacity), - borderRadius: BorderRadius.circular(smallBorderRadius), - ); - - static BoxDecoration glassmorphismDecoration(BuildContext context, {double opacity = 0.05}) => BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(opacity) - : Colors.black.withOpacity(opacity), - borderRadius: BorderRadius.circular(cardBorderRadius), - border: Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.05), - width: borderMedium, - ), - ); + static BoxDecoration primaryColorDecoration(BuildContext context, + {double opacity = 0.1}) => + BoxDecoration( + color: Theme.of(context).primaryColor.withValues(alpha: opacity), + borderRadius: BorderRadius.circular(smallBorderRadius), + ); + + static BoxDecoration glassmorphismDecoration(BuildContext context, + {double opacity = 0.05}) => + BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: opacity) + : Colors.black.withValues(alpha: opacity), + borderRadius: BorderRadius.circular(cardBorderRadius), + border: Border.all( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.05), + width: borderMedium, + ), + ); // Constantes d'animation static const Duration animationFast = Duration(milliseconds: 150); @@ -140,4 +170,4 @@ class StyleConstants { static const double maxModalHeight = 0.8; static const double minButtonHeight = 44.0; static const double standardAppBarHeight = 56.0; -} \ No newline at end of file +} diff --git a/lib/utils/text_utils.dart b/lib/utils/text_utils.dart index 33380b5..2e7833d 100644 --- a/lib/utils/text_utils.dart +++ b/lib/utils/text_utils.dart @@ -11,10 +11,11 @@ class TextUtils { : 0.0; } - static String truncateWallet(String wallet, {int prefixLength = 6, int suffixLength = 4}) { + static String truncateWallet(String wallet, + {int prefixLength = 6, int suffixLength = 4}) { if (wallet.length <= prefixLength + suffixLength) { return wallet; } - return wallet.substring(0, prefixLength) + '...' + wallet.substring(wallet.length - suffixLength); + return '${wallet.substring(0, prefixLength)}...${wallet.substring(wallet.length - suffixLength)}'; } } diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 8dae484..aad5711 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -6,17 +6,13 @@ import 'package:provider/provider.dart'; import 'package:realtoken_asset_tracker/managers/data_manager.dart'; import 'package:realtoken_asset_tracker/app_state.dart'; import 'package:realtoken_asset_tracker/generated/l10n.dart'; -import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/properties_details_page.dart'; -import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/rent_details_page.dart'; -import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/rmm_details_page.dart'; -import 'package:realtoken_asset_tracker/pages/dashboard/detailsPages/portfolio_details_page.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:realtoken_asset_tracker/utils/shimmer_utils.dart'; import 'package:realtoken_asset_tracker/utils/widget_factory.dart'; class UIUtils { static double getAppBarHeight(BuildContext context) { - double pixelRatio = MediaQuery.of(context).devicePixelRatio; // Ratio de densité + double pixelRatio = + MediaQuery.of(context).devicePixelRatio; // Ratio de densité double longestSide = MediaQuery.of(context).size.longestSide * pixelRatio; double shortestSide = MediaQuery.of(context).size.shortestSide * pixelRatio; @@ -39,13 +35,19 @@ class UIUtils { if (shortestSide >= 1500) { // Tablettes (toute orientation) - return orientation == Orientation.portrait ? kToolbarHeight : kToolbarHeight; // Exemple d'ajustement en paysage + return orientation == Orientation.portrait + ? kToolbarHeight + : kToolbarHeight; // Exemple d'ajustement en paysage } else if (longestSide > 2000) { // Grands téléphones - return orientation == Orientation.portrait ? kToolbarHeight + 40 : kToolbarHeight; // Exemple d'ajustement en paysage + return orientation == Orientation.portrait + ? kToolbarHeight + 40 + : kToolbarHeight; // Exemple d'ajustement en paysage } else { // Taille par défaut pour les téléphones standards - return orientation == Orientation.portrait ? kToolbarHeight : kToolbarHeight - 10; // Exemple d'ajustement en paysage + return orientation == Orientation.portrait + ? kToolbarHeight + : kToolbarHeight - 10; // Exemple d'ajustement en paysage } } else { // Par défaut pour desktop @@ -56,7 +58,8 @@ class UIUtils { static double getSliverAppBarHeight(BuildContext context) { double baseHeight = getAppBarHeight(context); - double pixelRatio = MediaQuery.of(context).devicePixelRatio; // Ratio de densité + double pixelRatio = + MediaQuery.of(context).devicePixelRatio; // Ratio de densité double longestSide = MediaQuery.of(context).size.longestSide * pixelRatio; double shortestSide = MediaQuery.of(context).size.shortestSide * pixelRatio; @@ -79,13 +82,21 @@ class UIUtils { if (shortestSide >= 1500) { // Tablettes - return orientation == Orientation.portrait ? baseHeight + 25 : baseHeight + 25; // Ajustement en paysage pour les tablettes + return orientation == Orientation.portrait + ? baseHeight + 25 + : baseHeight + 25; // Ajustement en paysage pour les tablettes } else if (longestSide > 2500) { // Grands téléphones - return orientation == Orientation.portrait ? baseHeight - 15 : baseHeight + 40; // Ajustement en paysage pour les grands téléphones + return orientation == Orientation.portrait + ? baseHeight - 15 + : baseHeight + + 40; // Ajustement en paysage pour les grands téléphones } else { // Taille par défaut pour téléphones standards - return orientation == Orientation.portrait ? baseHeight + 30 : baseHeight + 45; // Ajustement en paysage pour téléphones standards + return orientation == Orientation.portrait + ? baseHeight + 30 + : baseHeight + + 45; // Ajustement en paysage pour téléphones standards } } else { // Par défaut pour desktop @@ -122,7 +133,8 @@ class UIUtils { BuildContext context, { bool hasGraph = false, Widget? rightWidget, // Widget pour le graphique - Widget? headerRightWidget, // Widget pour l'icône dans l'en-tête (ex: paramètres) + Widget? + headerRightWidget, // Widget pour l'icône dans l'en-tête (ex: paramètres) }) { final appState = Provider.of(context); // Début de la logique responsive pour le graphique @@ -143,8 +155,12 @@ class UIUtils { borderRadius: BorderRadius.circular(16), ), elevation: 0.3, - shadowColor: Theme.of(context).brightness == Brightness.light ? Colors.black.withOpacity(0.1) : Colors.white.withOpacity(0.05), - color: Theme.of(context).brightness == Brightness.light ? Colors.white : Color(0xFF1C1C1E), + shadowColor: Theme.of(context).brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.1) + : Colors.white.withValues(alpha: 0.05), + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Color(0xFF1C1C1E), margin: EdgeInsets.symmetric(vertical: 6.0, horizontal: 2.0), child: Padding( padding: const EdgeInsets.all(12.0), @@ -163,7 +179,8 @@ class UIUtils { Container( padding: EdgeInsets.all(6), decoration: BoxDecoration( - color: getIconColor(title, context).withOpacity(0.15), + color: getIconColor(title, context) + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Icon( @@ -237,7 +254,8 @@ class UIUtils { // Fonction pour obtenir la couleur en fonction du titre traduit static Color getIconColor(String title, BuildContext context) { - final String translatedTitle = title.trim(); // Supprime les espaces éventuels + final String translatedTitle = + title.trim(); // Supprime les espaces éventuels if (translatedTitle == S.of(context).rents) { return Color(0xFF34C759); // Vert iOS @@ -262,7 +280,9 @@ class UIUtils { } } - static Widget buildValueBeforeText(BuildContext context, String? value, String text, bool isLoading, {bool highlightPercentage = false}) { + static Widget buildValueBeforeText( + BuildContext context, String? value, String text, bool isLoading, + {bool highlightPercentage = false}) { final appState = Provider.of(context); final theme = Theme.of(context); @@ -340,7 +360,8 @@ class UIUtils { ); } - static Widget buildTextWithShimmer(String? value, String text, bool isLoading, BuildContext context) { + static Widget buildTextWithShimmer( + String? value, String text, bool isLoading, BuildContext context) { // Utiliser le WidgetFactory optimisé return WidgetFactory.buildOptimizedTextWithShimmer( context: context, diff --git a/lib/utils/url_utils.dart b/lib/utils/url_utils.dart index 81f6fb8..d36840a 100644 --- a/lib/utils/url_utils.dart +++ b/lib/utils/url_utils.dart @@ -3,7 +3,8 @@ import 'package:url_launcher/url_launcher.dart'; class UrlUtils { static Future launchURL(String url) async { - debugPrint('Tentative d\'ouverture de l\'URL: $url'); // Log pour capturer l'URL + debugPrint( + 'Tentative d\'ouverture de l\'URL: $url'); // Log pour capturer l'URL final Uri uri = Uri.parse(url); try { if (await canLaunchUrl(uri)) { diff --git a/lib/utils/widget_factory.dart b/lib/utils/widget_factory.dart index 8b3e016..0f58347 100644 --- a/lib/utils/widget_factory.dart +++ b/lib/utils/widget_factory.dart @@ -10,7 +10,7 @@ class WidgetFactory { /// Widget pour créer un en-tête de section standardisé static Widget buildSectionHeader(BuildContext context, String title) { final theme = Theme.of(context); - + return Padding( padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), child: Row( @@ -50,7 +50,7 @@ class WidgetFactory { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY), child: Container( - color: backgroundColor?.withOpacity(opacity ?? 0.3), + color: backgroundColor?.withValues(alpha: opacity ?? 0.3), child: child, ), ), @@ -68,9 +68,9 @@ class WidgetFactory { filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( height: height, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.3) - : Colors.white.withOpacity(0.3), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.3), child: child, ), ), @@ -86,7 +86,7 @@ class WidgetFactory { double fontSize = 10, }) { final appState = Provider.of(context, listen: false); - + return ClipRRect( borderRadius: BorderRadius.circular(8), child: BackdropFilter( @@ -94,7 +94,7 @@ class WidgetFactory { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: backgroundColor ?? Colors.black.withOpacity(0.3), + color: backgroundColor ?? Colors.black.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -117,7 +117,7 @@ class WidgetFactory { double fontSize = 12, }) { final appState = Provider.of(context, listen: false); - + return ClipRRect( borderRadius: BorderRadius.circular(10), child: BackdropFilter( @@ -142,28 +142,33 @@ class WidgetFactory { } /// Décoration de carte standardisée avec ombre et gradient - static BoxDecoration buildCardDecoration(BuildContext context, { + static BoxDecoration buildCardDecoration( + BuildContext context, { double borderRadius = 16, bool withGradient = true, bool withShadow = true, }) { return BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), - boxShadow: withShadow ? [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ] : null, - gradient: withGradient ? LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).cardColor, - Theme.of(context).cardColor.withOpacity(0.8), - ], - ) : null, + boxShadow: withShadow + ? [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ] + : null, + gradient: withGradient + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).cardColor, + Theme.of(context).cardColor.withValues(alpha: 0.8), + ], + ) + : null, color: withGradient ? null : Theme.of(context).cardColor, ); } @@ -172,21 +177,21 @@ class WidgetFactory { static BoxDecoration buildDetailContainerDecoration(BuildContext context) { final theme = Theme.of(context); return BoxDecoration( - color: theme.brightness == Brightness.light - ? Colors.grey.shade50 - : theme.cardColor.withOpacity(0.7), + color: theme.brightness == Brightness.light + ? Colors.grey.shade50 + : theme.cardColor.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(16), border: Border.all( - color: theme.brightness == Brightness.light - ? Colors.grey.shade200 + color: theme.brightness == Brightness.light + ? Colors.grey.shade200 : theme.dividerColor, width: 1.0, ), boxShadow: [ BoxShadow( - color: theme.brightness == Brightness.light - ? Colors.black.withOpacity(0.02) - : Colors.black.withOpacity(0.1), + color: theme.brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.02) + : Colors.black.withValues(alpha: 0.1), blurRadius: 3, offset: const Offset(0, 1), ), @@ -195,14 +200,15 @@ class WidgetFactory { } /// Décoration pour les headers avec gradient primaire - static BoxDecoration buildPrimaryGradientDecoration(BuildContext context, { + static BoxDecoration buildPrimaryGradientDecoration( + BuildContext context, { double borderRadius = 24, }) { final theme = Theme.of(context); return BoxDecoration( gradient: LinearGradient( colors: [ - theme.primaryColor.withOpacity(0.9), + theme.primaryColor.withValues(alpha: 0.9), theme.primaryColor, ], begin: Alignment.topCenter, @@ -212,7 +218,7 @@ class WidgetFactory { borderRadius: BorderRadius.circular(borderRadius), boxShadow: [ BoxShadow( - color: theme.primaryColor.withOpacity(0.2), + color: theme.primaryColor.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 4), spreadRadius: -2, @@ -240,7 +246,8 @@ class WidgetFactory { } /// InputDecoration standardisée pour les champs de texte - static InputDecoration buildStandardInputDecoration(BuildContext context, { + static InputDecoration buildStandardInputDecoration( + BuildContext context, { String? hintText, IconData? prefixIcon, Widget? suffixIcon, @@ -258,7 +265,7 @@ class WidgetFactory { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: Theme.of(context).dividerColor.withOpacity(0.5), + color: Theme.of(context).dividerColor.withValues(alpha: 0.5), ), ), focusedBorder: OutlineInputBorder( @@ -287,7 +294,7 @@ class WidgetFactory { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, -2), ), @@ -307,9 +314,9 @@ class WidgetFactory { width: 36, height: 4, decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), ); @@ -328,20 +335,24 @@ class WidgetFactory { final appState = Provider.of(context, listen: false); final theme = Theme.of(context); - final defaultValueStyle = valueStyle ?? TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: theme.textTheme.bodyLarge?.color, - letterSpacing: -0.3, - height: 1.1, - ); + final defaultValueStyle = valueStyle ?? + TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: theme.textTheme.bodyLarge?.color, + letterSpacing: -0.3, + height: 1.1, + ); - final defaultLabelStyle = labelStyle ?? TextStyle( - fontSize: 14, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, - letterSpacing: -0.2, - height: 1.1, - ); + final defaultLabelStyle = labelStyle ?? + TextStyle( + fontSize: 14, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, + letterSpacing: -0.2, + height: 1.1, + ); return Padding( padding: const EdgeInsets.symmetric(vertical: 2), @@ -374,8 +385,10 @@ class WidgetFactory { final appState = Provider.of(context, listen: false); final theme = Theme.of(context); - String formattedAmount = showAmounts - ? (isPositive ? "+ ${value.toStringAsFixed(2)} $symbol" : "- ${value.toStringAsFixed(2)} $symbol") + String formattedAmount = showAmounts + ? (isPositive + ? "+ ${value.toStringAsFixed(2)} $symbol" + : "- ${value.toStringAsFixed(2)} $symbol") : (isPositive ? "+ " : "- ") + ('*' * 10); Color valueColor = isPositive @@ -414,7 +427,9 @@ class WidgetFactory { style: TextStyle( fontSize: 13, letterSpacing: -0.2, - color: theme.brightness == Brightness.light ? Colors.black54 : Colors.white70, + color: theme.brightness == Brightness.light + ? Colors.black54 + : Colors.white70, ), ), ], @@ -438,7 +453,7 @@ class WidgetFactory { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 1, offset: const Offset(0, 1), ), @@ -527,19 +542,16 @@ class WidgetFactory { return Column( children: [ - onTap != null - ? GestureDetector(onTap: onTap, child: content) - : content, + onTap != null ? GestureDetector(onTap: onTap, child: content) : content, if (!isLast) Padding( padding: const EdgeInsets.only(left: 12), child: Divider( - height: 1, - thickness: 0.5, - color: Colors.grey.withOpacity(0.3) - ), + height: 1, + thickness: 0.5, + color: Colors.grey.withValues(alpha: 0.3)), ), ], ); } -} \ No newline at end of file +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 29ee910..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}