diff --git a/assets/translations/en.json b/assets/translations/en.json index e00f8aa4708b3edd25349cef15e068ae36f7b204..5b8ff659da109024e7b3daba9f80648715429250 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -195,7 +195,7 @@ "migrateAccount": "Migrate account", "migrateIdentity": "Migrate identity", "identityMigration": "Identity migration", - "areYouSureMigrateIdentity": "Are you sure you want to permanently migrate identity **{}** with balance of **{}** ?", + "areYouSureMigrateIdentity": "Are you sure you want to permanently migrate identity **{}** with balance of", "someoneCreatedYourIdentity": "Someone created your {} identity !", "confirmMyIdentity": "Confirm my identity", "revokeMyIdentity": "Revoke my identity", @@ -235,7 +235,7 @@ "fundsUnavailable": "Insufficient funds", "addressNotBelongToMnemonic": "The address you provided does not belong to this recovery sentence", "enterYourNewMnemonic": "Enter your new recovery sentence", - "enterYourNewAddress": "Enter your new address {}", + "enterYourNewAddress": "Enter your new {} address", "youCanMigrateThisIdentity": "You can migrate this identity !", "identityMigrated": "Identity migrated", "passwordTooSimple": "Your password is to simple", @@ -254,5 +254,30 @@ "optionalComment": "Optional comment", "searchContacts": "Search contacts", "advancedFeature": "Advanced feature reserved for experienced users", - "editWalletName": "Edit wallet name" + "editWalletName": "Edit wallet name", + "cesiumCredentials": "Cesium Credentials", + "accountInformation": "Account Information", + "migrationConfirmTitle": "Migration Confirmation", + "migrationConfirmBalanceOnly": "All your {} will be transferred to account {}.", + "migrationConfirmWithIdentity": "All your {} and your identity will be transferred to account {}.", + "accountMigration": "Account migration", + "areYouSureCreateIdentityOnAddress": "Are you sure you want to create an identity on address:", + "identityCreation": "Identity Creation", + "chooseIdentityName": "Choose an identity name", + "identityInDuniterNetwork": "Your identity in the {} libre currency network", + "identityExplanation": "Your identity is your unique presence in the web of trust. It allows you to be recognized by other members and actively participate in the community.", + "identityNameUnique": "The chosen name must be unique in the network", + "identityNameSearchable": "Other members will be able to find you easily with this name", + "identityNamePermanent": "This name cannot be modified once validated", + "enterIdentityName": "Enter your identity name", + "identityNameTooShort": "Name must be at least 3 characters long", + "identityNameTooLong": "Name must not exceed 32 characters", + "confirmIdentityCreation": "Identity Confirmation", + "confirmIdentityNameChoice": "Are you sure you want to use \"{}\" as your identity name? This choice is permanent.", + "identityNameNoSpaces": "Name must not contain spaces", + "areYouSureYouWantToRevokeIdentity": "Are you sure you want to revoke your identity? This action is permanent.", + "confirmationTitle": "Confirmation", + "dangerZone": "Danger zone", + "manual": "Manual", + "select": "Select" } diff --git a/assets/translations/es.json b/assets/translations/es.json index f4b0c39af1ea803b3f6b7d7914d800272839a44e..56684978c8b68db7a556a9de36c00b7b73ca1723 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -196,7 +196,7 @@ "migrateAccount": "Migrar cuenta", "migrateIdentity": "Migrar identidad", "identityMigration": "Migración de la identidad", - "areYouSureMigrateIdentity": "¿Estás seguro de que quieres migrar permanentemente la identidad **{}** con saldo de **{}**?", + "areYouSureMigrateIdentity": "¿Estás seguro de que quieres migrar permanentemente la identidad **{}** con saldo de", "someoneCreatedYourIdentity": "¡ Alguien ha creado tu {} identidad !", "confirmMyIdentity": "Confirmar mi identidad", "revokeMyIdentity": "Revocar mi identidad", @@ -255,5 +255,30 @@ "optionalComment": "Comentario opcional", "searchContacts": "Buscar contactos", "advancedFeature": "Función avanzada reservada para usuarios experimentados", - "editWalletName": "Editar nombre de la cartera" + "editWalletName": "Editar nombre de la cartera", + "cesiumCredentials": "Credenciales de Cesium", + "accountInformation": "Información de la cuenta", + "migrationConfirmTitle": "Confirmación de migración", + "migrationConfirmBalanceOnly": "Todos sus {} serán transferidos a la cuenta {}.", + "migrationConfirmWithIdentity": "Todos sus {} y su identidad serán transferidos a la cuenta {}.", + "accountMigration": "Migración de cuenta", + "areYouSureCreateIdentityOnAddress": "¿Está seguro de que desea crear una identidad en la dirección:", + "identityCreation": "Creación de identidad", + "chooseIdentityName": "Elegir un nombre de identidad", + "identityInDuniterNetwork": "Tu identidad en la red de moneda libre {}", + "identityExplanation": "Tu identidad es tu presencia única en la red de confianza. Te permite ser reconocido por otros miembros y participar activamente en la comunidad.", + "identityNameUnique": "El nombre elegido debe ser único en la red", + "identityNameSearchable": "Otros miembros podrán encontrarte fácilmente con este nombre", + "identityNamePermanent": "Este nombre no se puede modificar una vez validado", + "enterIdentityName": "Ingresa tu nombre de identidad", + "identityNameTooShort": "El nombre debe tener al menos 3 caracteres", + "identityNameTooLong": "El nombre no debe exceder los 32 caracteres", + "confirmIdentityCreation": "Confirmación de identidad", + "confirmIdentityNameChoice": "¿Estás seguro de querer usar \"{}\" como tu nombre de identidad? Esta elección es permanente.", + "identityNameNoSpaces": "El nombre no debe contener espacios", + "areYouSureYouWantToRevokeIdentity": "¿Estás seguro de que quieres revocar tu identidad? Esta acción es permanente.", + "confirmationTitle": "Confirmación", + "dangerZone": "Zona peligrosa", + "manual": "Manual", + "select": "Seleccionar" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 22e3610af0b4c7282a1dd659eb8383fdcfd6fc59..617bdb672939156475b003f979d3656d771a11c7 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -195,7 +195,7 @@ "migrateAccount": "Migrer le compte", "migrateIdentity": "Migrer l'identité", "identityMigration": "Migration de l'identité", - "areYouSureMigrateIdentity": "Êtes-vous certain de vouloir migrer définitivement l'identité **{}** et son solde de **{}** ?", + "areYouSureMigrateIdentity": "Êtes-vous certain de vouloir migrer définitivement l'identité **{}** et son solde de", "someoneCreatedYourIdentity": "Quelqu'un a créé votre identité {} !", "confirmMyIdentity": "Confirmer mon identité", "revokeMyIdentity": "Révoquer mon identité", @@ -254,5 +254,30 @@ "optionalComment": "Commentaire optionnel", "searchContacts": "Rechercher des contacts", "advancedFeature": "Fonctionnalité avancée réservée aux utilisateurs expérimentés", - "editWalletName": "Modifier le nom du portefeuille" + "editWalletName": "Modifier le nom du portefeuille", + "cesiumCredentials": "Identifiants Cesium", + "accountInformation": "Informations du compte", + "migrationConfirmTitle": "Confirmation de migration", + "migrationConfirmBalanceOnly": "Tous vos {} vont être transférés vers le compte {}.", + "migrationConfirmWithIdentity": "Tous vos {} et votre identité vont être transférés vers le compte {}.", + "accountMigration": "Migration du compte", + "areYouSureCreateIdentityOnAddress": "Êtes-vous sûr de vouloir créer une identité sur l'adresse :", + "identityCreation": "Création d'identité", + "chooseIdentityName": "Choisir un nom d'identité", + "identityInDuniterNetwork": "Votre identité dans la monnaie libre {}", + "identityExplanation": "Votre identité est votre présence unique dans la toile de confiance. Elle vous permet d'être reconnu par les autres membres et de participer activement à la communauté.", + "identityNameUnique": "Le nom choisi doit être unique dans le réseau", + "identityNameSearchable": "Les autres membres pourront vous trouver facilement avec ce nom", + "identityNamePermanent": "Ce nom ne pourra pas être modifié une fois validé", + "enterIdentityName": "Entrez votre nom d'identité", + "identityNameTooShort": "Le nom doit contenir au moins 3 caractères", + "identityNameTooLong": "Le nom ne doit pas dépasser 32 caractères", + "confirmIdentityCreation": "Confirmation de l'identité", + "confirmIdentityNameChoice": "Êtes-vous sûr de vouloir utiliser \"{}\" comme nom d'identité ? Ce choix est définitif.", + "identityNameNoSpaces": "Le nom ne doit pas contenir d'espaces", + "areYouSureYouWantToRevokeIdentity": "Êtes-vous sûr de vouloir révoquer votre identité ? Cette action est définitive.", + "confirmationTitle": "Confirmation", + "dangerZone": "Zone dangereuse", + "manual": "Manuel", + "select": "Sélectionner" } diff --git a/assets/translations/it.json b/assets/translations/it.json index 893a77358dcb3314171684502052f77fe1477d4f..aebeda011e0c667097a42acda26ec15448f9139e 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -191,7 +191,7 @@ "migrateAccount": "Migra conto", "migrateIdentity": "Migra identitá", "identityMigration": "Migrazione dell'identitá", - "areYouSureMigrateIdentity": "Sei sicuro che vuoi permanentemente migrare l'identitá **{}** con saldo di **{}**?", + "areYouSureMigrateIdentity": "Sei sicuro che vuoi permanentemente migrare l'identitá **{}** con saldo di", "someoneCreatedYourIdentity": "Qualcuno ha creato la tua {} identitá !", "confirmMyIdentity": "Conferma la mia identitá", "revokeMyIdentity": "Revoca la mia identitá", diff --git a/lib/models/membership_renewal.dart b/lib/models/membership_renewal.dart index ab076ff936edfdf27588068076d9679fbfe7e969..e68a735304e082cede314f3c8c7a9ed5a77e1942 100644 --- a/lib/models/membership_renewal.dart +++ b/lib/models/membership_renewal.dart @@ -8,7 +8,7 @@ import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:gecko/utils.dart'; -import 'package:gecko/widgets/commons/common_elements.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; import 'package:provider/provider.dart'; import 'package:gecko/models/membership_status.dart'; @@ -40,7 +40,12 @@ class MembershipRenewal { } static Future<void> executeRenewal(BuildContext context, String address) async { - final answer = await confirmPopup(context, 'areYouSureYouWantToRenewMembership'.tr()) ?? false; + final answer = await showConfirmationDialog( + context: context, + message: 'areYouSureYouWantToRenewMembership'.tr(), + type: ConfirmationDialogType.question, + ) ?? + false; if (!answer) return; final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); diff --git a/lib/models/queries_indexer.dart b/lib/models/queries_indexer.dart index 6e467525b8757223051c2efe5de02143bdf124b3..67892a2ad8da7cf43e1ec4188c2e12795e21df78 100644 --- a/lib/models/queries_indexer.dart +++ b/lib/models/queries_indexer.dart @@ -118,7 +118,7 @@ query ($address: String!) { const isIdtyExistQ = r''' query ($name: String!) { - identityConnection(where: {name: {_eq: ""}}) { + identityConnection(where: {name: {_eq: $name}}) { edges { node { name diff --git a/lib/models/wallet_header_data.dart b/lib/models/wallet_header_data.dart new file mode 100644 index 0000000000000000000000000000000000000000..7644a84f3445ef41c011f5ab408d1cf35ac2299e --- /dev/null +++ b/lib/models/wallet_header_data.dart @@ -0,0 +1,28 @@ +class WalletHeaderData { + final bool hasIdentity; + final bool isOwner; + final String? walletName; + final BigInt balance; + final List<int> certCount; + + WalletHeaderData({ + required this.hasIdentity, + required this.isOwner, + this.walletName, + required this.balance, + required this.certCount, + }); + + // Pour comparer si les données ont changé + bool equals(WalletHeaderData other) { + if (certCount.isEmpty || other.certCount.isEmpty) { + return hasIdentity == other.hasIdentity && isOwner == other.isOwner && walletName == other.walletName && balance == other.balance; + } + return hasIdentity == other.hasIdentity && + isOwner == other.isOwner && + walletName == other.walletName && + balance == other.balance && + certCount[0] == other.certCount[0] && + certCount[1] == other.certCount[1]; + } +} diff --git a/lib/providers/duniter_indexer.dart b/lib/providers/duniter_indexer.dart index 34b795be196f17ad453b50f0b036fb79ea7ff933..83b28839884f618e62986288a21bd76ea29f0493 100644 --- a/lib/providers/duniter_indexer.dart +++ b/lib/providers/duniter_indexer.dart @@ -284,8 +284,7 @@ class DuniterIndexer with ChangeNotifier { 'name': name, }; final result = await _execQuery(isIdtyExistQ, variables); - log.d(result.data); - return result.data?['identity']?.isNotEmpty ?? false; + return result.data?['identityConnection']['edges']?.isNotEmpty ?? false; } Future<DateTime> getBlockStart() async { diff --git a/lib/providers/my_wallets.dart b/lib/providers/my_wallets.dart index d42fe8f9ffea4805cb3dcb6fde00e144de5edaa0..404b89bc4b7fb4c0feb4c7dcf08ead7650aed704 100644 --- a/lib/providers/my_wallets.dart +++ b/lib/providers/my_wallets.dart @@ -8,7 +8,7 @@ import 'package:gecko/globals.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; -import 'package:gecko/widgets/commons/common_elements.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; @@ -123,7 +123,11 @@ class MyWalletsProvider with ChangeNotifier { try { log.w('DELETE ALL WALLETS ?'); - final bool? answer = await (confirmPopup(context, 'areYouSureForgetAllChests'.tr())); + final bool? answer = await showConfirmationDialog( + context: context, + message: 'areYouSureForgetAllChests'.tr(), + type: ConfirmationDialogType.warning, + ); if (answer!) { await walletBox.clear(); await chestBox.clear(); diff --git a/lib/providers/substrate_sdk.dart b/lib/providers/substrate_sdk.dart index beb6e3a58c952871c770eebc3fa51c4103239a23..39c7a75446b60985c20c477d6aad49a063cbf9b5 100644 --- a/lib/providers/substrate_sdk.dart +++ b/lib/providers/substrate_sdk.dart @@ -54,6 +54,14 @@ class SubstrateSdk with ChangeNotifier { Map<String, List<int>> certsCounterCache = {}; Map<String, List> oldOwnerKeys = {}; + // Cache pour idtyStatus + final Map<String, IdtyStatus> _idtyStatusCache = {}; + + // Getter public pour accéder au statut en cache + IdtyStatus? getCachedIdtyStatus(String address) { + return _idtyStatusCache[address]; + } + ///////////////////////////////////// ////////// 1: API METHODS /////////// ///////////////////////////////////// @@ -203,20 +211,28 @@ class SubstrateSdk with ChangeNotifier { } Future<List<int>> getCertsCounter(String address) async { + // On fait toujours la requête en background final idtyIndex = await _getIdentityIndexOf(address); if (idtyIndex == null) { - certsCounterCache.update(address, (_) => [], ifAbsent: () => []); - return []; + final emptyList = <int>[]; + // Si le compteur a changé (était non vide avant), on notifie + if (certsCounterCache[address]?.isNotEmpty == true) { + certsCounterCache[address] = emptyList; + notifyListeners(); + } + return emptyList; } + final certsReceiver = await _getStorage('certification.storageIdtyCertMeta($idtyIndex)') ?? []; + final List<int> newCerts = [certsReceiver['receivedCount'] as int, certsReceiver['issuedCount'] as int]; - try { - certsCounterCache.update(address, (_) => [certsReceiver['receivedCount'] as int, certsReceiver['issuedCount'] as int], - ifAbsent: () => [certsReceiver['receivedCount'] as int, certsReceiver['issuedCount'] as int]); - } catch (e) { - log.e(e); + // Si le compteur a changé, on met à jour le cache et on notifie + if (certsCounterCache[address]?.length != 2 || certsCounterCache[address]![0] != newCerts[0] || certsCounterCache[address]![1] != newCerts[1]) { + certsCounterCache[address] = newCerts; + notifyListeners(); } - return certsCounterCache[address]!; + + return certsCounterCache[address] ?? newCerts; } Future<DateTime?> membershipExpireIn(String address) async { @@ -434,6 +450,20 @@ class SubstrateSdk with ChangeNotifier { }; Future<IdtyStatus> idtyStatus(String address) async { + // On fait toujours la requête en background + final status = await _idtyStatus(address); + + // Si le statut a changé, on met à jour le cache et on notifie + if (_idtyStatusCache[address] != status) { + _idtyStatusCache[address] = status; + notifyListeners(); + } + + // On retourne le statut du cache s'il existe, sinon le nouveau statut + return _idtyStatusCache[address] ?? status; + } + + Future<IdtyStatus> _idtyStatus(String address) async { final idtyIndex = await _getIdentityIndexOf(address); if (idtyIndex == null) return IdtyStatus.none; final idtyStatus = await idtyStatusByIndex(idtyIndex); @@ -962,22 +992,22 @@ class SubstrateSdk with ChangeNotifier { late String tx2; late String tx3; + // Computed amount in absolute value + final int amountUnit = (amount * (isUdUnit ? 1000 : 100)).toInt(); + // Préparer la transaction de transfert - if (amount == -1 || amount == defaultWalletBalance) { + if (amount == -1 || amountUnit == defaultWalletBalance) { palette = 'balances'; call = 'transferAll'; txOptions = [destAddress, false]; tx2 = 'api.tx.balances.transferAll("$destAddress", false)'; } else { - late int amountUnit; if (isUdUnit) { palette = 'universalDividend'; call = 'transferUd'; - amountUnit = (amount * 1000).toInt(); } else { palette = 'balances'; call = 'transferKeepAlive'; - amountUnit = (amount * 100).toInt(); } txOptions = [destAddress, amountUnit]; tx2 = 'api.tx.$palette.$call("$destAddress", $amountUnit)'; @@ -1010,7 +1040,7 @@ class SubstrateSdk with ChangeNotifier { to: destAddress, amount: amount, ); - log.d('txInfoo: ${txInfo.module}.${txInfo.call!} -- $txOptions -- $rawParams'); + log.d('txInfoo: ${txInfo.module}.${txInfo.call} -- $txOptions -- $rawParams'); _executeCall(transactionContent, txInfo, txOptions, password, rawParams); } diff --git a/lib/providers/wallet_options.dart b/lib/providers/wallet_options.dart index d7aaa7291a7c3bb74fd81adcae6a4e61a98f0830..231ec297fd82b22b68bca586f38419fdce507971 100644 --- a/lib/providers/wallet_options.dart +++ b/lib/providers/wallet_options.dart @@ -13,8 +13,8 @@ import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/v2s_datapod.dart'; import 'package:gecko/utils.dart'; -import 'package:gecko/widgets/commons/common_elements.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:image_cropper/image_cropper.dart'; @@ -49,7 +49,11 @@ class WalletOptionsProvider with ChangeNotifier { Future<int> deleteWallet(context, WalletData wallet) async { final sub = Provider.of<SubstrateSdk>(context, listen: false); final datapod = Provider.of<V2sDatapodProvider>(context, listen: false); - final bool? answer = await (confirmPopup(context, 'areYouSureToForgetWallet'.tr(args: [wallet.name!]))); + final bool? answer = await showConfirmationDialog( + context: context, + message: 'areYouSureToForgetWallet'.tr(args: [wallet.name!]), + type: ConfirmationDialogType.warning, + ); if (answer ?? false) { //Check if balance is null diff --git a/lib/screens/debug_screen.dart b/lib/screens/debug_screen.dart index 32755c5ae637870537da1a686e3e6717a5fdb9a6..29f0640c46b65ac195b4e989b38adb776d85fdd1 100644 --- a/lib/screens/debug_screen.dart +++ b/lib/screens/debug_screen.dart @@ -3,6 +3,7 @@ import 'package:gecko/globals.dart'; import 'package:flutter/material.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/providers/substrate_sdk.dart'; +import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:provider/provider.dart'; class DebugScreen extends StatelessWidget { @@ -11,50 +12,145 @@ class DebugScreen extends StatelessWidget { @override Widget build(BuildContext context) { final sub = Provider.of<SubstrateSdk>(context); + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.height < 700; return Scaffold( - backgroundColor: backgroundColor, - appBar: AppBar( - toolbarHeight: scaleSize(57), title: const Text('Debug screen')), - body: SafeArea( - child: Column(children: <Widget>[ - const SizedBox(height: 40), - Center( - child: Column( - children: [ - Text( - 'node: ${sub.getConnectedEndpoint()}', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + backgroundColor: backgroundColor, + appBar: GeckoAppBar('Debug'), + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: scaleSize(24)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Section Nœud + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), - const SizedBox(height: 15), - Text( - 'blockN'.tr(args: [ - sub.blocNumber.toString() - ]), //'bloc N°${sub.blocNumber}', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + child: Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.dns_rounded, + color: orangeC, + size: scaleSize(24), + ), + ScaledSizedBox(width: 12), + Text( + 'currencyNode'.tr(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ScaledSizedBox(height: 12), + Text( + 'node: ${sub.getConnectedEndpoint()}', + style: scaledTextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ScaledSizedBox(height: 8), + Text( + 'blockN'.tr(args: [sub.blocNumber.toString()]), + style: scaledTextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ), ), - const SizedBox(height: 20), - SizedBox( - height: 50, - width: 210, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - elevation: 4, - backgroundColor: orangeC, - ), - onPressed: () async => await sub.spawnBlock(), - child: const Text( - 'Spawn a bloc', - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.w600), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Section Actions + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), ), + ], + ), + child: Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.build_rounded, + color: orangeC, + size: scaleSize(24), + ), + ScaledSizedBox(width: 12), + Text( + 'Actions', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ScaledSizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: orangeC, + padding: EdgeInsets.symmetric(vertical: scaleSize(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async => await sub.spawnBlock(), + child: Text( + 'Spawn a bloc', + style: scaledTextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ], ), ), - ], - ), + ), + ], ), - ]), - )); + ), + ), + ), + ); } } diff --git a/lib/screens/identity/confirm_identity.dart b/lib/screens/identity/confirm_identity.dart new file mode 100644 index 0000000000000000000000000000000000000000..591beb30c36b0be87558e8b1ec617e49c31f07a9 --- /dev/null +++ b/lib/screens/identity/confirm_identity.dart @@ -0,0 +1,235 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gecko/globals.dart'; +import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/models/widgets_keys.dart'; +import 'package:gecko/providers/duniter_indexer.dart'; +import 'package:gecko/providers/my_wallets.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; +import 'package:gecko/screens/transaction_in_progress.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; +import 'package:gecko/widgets/commons/wallet_app_bar.dart'; +import 'package:provider/provider.dart'; + +class ConfirmIdentityScreen extends StatefulWidget { + const ConfirmIdentityScreen({super.key, required this.address}); + final String address; + + @override + State<ConfirmIdentityScreen> createState() => _ConfirmIdentityScreenState(); +} + +class _ConfirmIdentityScreenState extends State<ConfirmIdentityScreen> { + final TextEditingController _identityNameController = TextEditingController(); + bool _canValidate = false; + String _errorMessage = ''; + + @override + void dispose() { + _identityNameController.dispose(); + super.dispose(); + } + + Future<void> _validateIdentityName(DuniterIndexer duniterIndexer) async { + final name = _identityNameController.text.trim(); + final idtyExist = await duniterIndexer.isIdtyExist(name); + final hasNoSpaces = !name.contains(' '); + final isValid = !idtyExist && hasNoSpaces && name.length >= 3 && name.length <= 32; + + setState(() { + _canValidate = isValid; + if (idtyExist) { + _errorMessage = 'thisIdentityAlreadyExist'.tr(); + } else if (!hasNoSpaces) { + _errorMessage = 'identityNameNoSpaces'.tr(); + } else if (name.length < 3) { + _errorMessage = 'identityNameTooShort'.tr(); + } else if (name.length > 32) { + _errorMessage = 'identityNameTooLong'.tr(); + } else { + _errorMessage = ''; + } + }); + } + + Future<void> _confirmIdentity(BuildContext context) async { + final name = _identityNameController.text.trim(); + final navigatorState = Navigator.of(context); + final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); + final sub = Provider.of<SubstrateSdk>(context, listen: false); + + // Afficher le dialogue de confirmation + final confirmed = await showConfirmationDialog( + context: context, + type: ConfirmationDialogType.info, + message: 'confirmIdentityNameChoice'.tr(args: [name]), + ); + + if (confirmed != true) return; + + if (!await myWalletProvider.askPinCode()) return; + + final transactionId = await sub.confirmIdentity(widget.address, name, myWalletProvider.pinCode); + + if (!mounted) return; + navigatorState.pop(); + + navigatorState.push( + MaterialPageRoute( + builder: (context) => TransactionInProgress( + transactionId: transactionId, + transType: 'comfirmIdty', + fromAddress: widget.address, + toAddress: widget.address, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.height < 700; + + return Scaffold( + backgroundColor: Colors.white, + appBar: WalletAppBar( + address: widget.address, + title: 'chooseIdentityName'.tr(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(scaleSize(16)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête avec icône + Center( + child: Container( + width: scaleSize(isSmallScreen ? 60 : 80), + height: scaleSize(isSmallScreen ? 60 : 80), + decoration: BoxDecoration( + color: orangeC.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.person_outline, + size: scaleSize(isSmallScreen ? 30 : 40), + color: orangeC, + ), + ), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 32), + + // Titre principal + Text( + 'identityInDuniterNetwork'.tr(args: [currencyName]), + style: scaledTextStyle( + fontSize: isSmallScreen ? 20 : 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Texte explicatif + Text( + 'identityExplanation'.tr(), + style: scaledTextStyle(fontSize: isSmallScreen ? 14 : 16), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Points importants + ...[ + 'identityNameUnique'.tr(), + 'identityNameSearchable'.tr(), + 'identityNamePermanent'.tr(), + ].map((text) => Padding( + padding: EdgeInsets.only(bottom: scaleSize(isSmallScreen ? 8 : 12)), + child: Row( + children: [ + Icon(Icons.check_circle, color: orangeC, size: scaleSize(isSmallScreen ? 16 : 20)), + ScaledSizedBox(width: isSmallScreen ? 8 : 12), + Expanded( + child: Text( + text, + style: scaledTextStyle(fontSize: isSmallScreen ? 14 : 16), + ), + ), + ], + ), + )), + ScaledSizedBox(height: isSmallScreen ? 24 : 32), + + // Champ de saisie + TextField( + key: keyEnterIdentityUsername, + controller: _identityNameController, + onChanged: (_) => _validateIdentityName(duniterIndexer), + textInputAction: TextInputAction.done, + onSubmitted: (_) { + if (_canValidate) { + _confirmIdentity(context); + } + }, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'^ ')), + ], + decoration: InputDecoration( + hintText: 'enterIdentityName'.tr(), + errorText: _errorMessage.isNotEmpty ? _errorMessage : null, + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: scaleSize(16), + vertical: scaleSize(12), + ), + ), + style: scaledTextStyle(fontSize: isSmallScreen ? 14 : 16), + ), + ], + ), + ), + ), + ), + // Bouton de validation en position fixe + Padding( + padding: EdgeInsets.all(scaleSize(16)), + child: SizedBox( + width: double.infinity, + height: scaleSize(isSmallScreen ? 44 : 50), + child: ElevatedButton( + key: keyConfirm, + onPressed: _canValidate ? () => _confirmIdentity(context) : null, + style: ElevatedButton.styleFrom( + backgroundColor: orangeC, + disabledBackgroundColor: Colors.grey[300], + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'validate'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 16, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/myWallets/import_g1_v1.dart b/lib/screens/myWallets/import_g1_v1.dart index fe036aee250215c7c6bbdef86e627f5c4b85e2fc..98e5ff8c9e665afe0bd345de777680c3c8c44ed3 100644 --- a/lib/screens/myWallets/import_g1_v1.dart +++ b/lib/screens/myWallets/import_g1_v1.dart @@ -18,7 +18,9 @@ import 'package:gecko/utils.dart'; import 'package:gecko/widgets/certifications.dart'; import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:gecko/widgets/idty_status.dart'; +import 'package:gecko/widgets/balance_display.dart'; import 'package:provider/provider.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; class ImportG1v1 extends StatelessWidget { const ImportG1v1({super.key}); @@ -61,229 +63,388 @@ class ImportG1v1 extends StatelessWidget { final statusData = status.data!; - final bool isUdUnit = configBox.get('isUdUnit') ?? false; - final unit = isUdUnit ? 'ud'.tr(args: ['']) : currencyName; - - return Column(children: <Widget>[ - ScaledSizedBox(height: 10), - TextFormField( - key: keyCesiumId, - autofocus: true, - autocorrect: false, - onChanged: (text) { - if (debounce?.isActive ?? false) { - debounce!.cancel(); - } - debounce = Timer(const Duration(milliseconds: debouneTime), () { - if (sub.csSalt.text != '' && sub.csPassword.text != '') { - sub.reload(); - sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); - } - }); - }, - keyboardType: TextInputType.text, - controller: sub.csSalt, - obscureText: !sub.isCesiumIDVisible, - style: scaledTextStyle(fontSize: 13), - decoration: InputDecoration( - hintText: 'enterCesiumId'.tr(), - hintStyle: scaledTextStyle(fontSize: 13), - suffixIcon: IconButton( - key: keyCesiumIdVisible, - icon: Icon( - sub.isCesiumIDVisible ? Icons.visibility_off : Icons.visibility, - color: Colors.black, - size: scaleSize(22), - ), - onPressed: () { - sub.cesiumIDisVisible(); - }, - ), - ), - ), - ScaledSizedBox(height: 7), - TextFormField( - key: keyCesiumPassword, - autofocus: true, - autocorrect: false, - onChanged: (text) { - if (debounce?.isActive ?? false) { - debounce!.cancel(); - } - debounce = Timer(const Duration(milliseconds: debouneTime), () { - sub.g1V1NewAddress = ''; - if (sub.csSalt.text != '' && sub.csPassword.text != '') { - sub.reload(); - sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); - } - }); - }, - keyboardType: TextInputType.text, - controller: sub.csPassword, - obscureText: !sub.isCesiumIDVisible, - style: scaledTextStyle(fontSize: 13), - decoration: InputDecoration( - hintText: 'enterCesiumPassword'.tr(), - hintStyle: scaledTextStyle(fontSize: 13), - suffixIcon: IconButton( - icon: Icon( - sub.isCesiumIDVisible ? Icons.visibility_off : Icons.visibility, - color: Colors.black, - size: scaleSize(22), + return SingleChildScrollView( + padding: EdgeInsets.all(scaleSize(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + // Section des identifiants Cesium + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - onPressed: () { - sub.cesiumIDisVisible(); - }, - ), - ), - ), - ScaledSizedBox(height: 20), - Visibility( - visible: sub.g1V1OldPubkey != '', - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - GestureDetector( - key: keyCopyPubkey, - onTap: () { - Clipboard.setData(ClipboardData(text: sub.g1V1OldPubkey)); - snackCopyKey(context); - }, - child: Text( - 'v1: ${getShortPubkey(sub.g1V1OldPubkey)}', - style: scaledTextStyle(fontSize: 15, fontWeight: FontWeight.w600, fontFamily: 'Monospace'), + child: Padding( + padding: EdgeInsets.all(scaleSize(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'cesiumCredentials'.tr(), + style: scaledTextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), - ), - ScaledSizedBox(height: 5), - GestureDetector( - key: keyCopyAddress, - onTap: () { - Clipboard.setData(ClipboardData(text: sub.g1V1OldPubkey)); - snackCopyKey(context); - }, - child: Text( - 'v2: ${getShortPubkey(sub.g1V1NewAddress)}', - style: scaledTextStyle(fontSize: 15, fontWeight: FontWeight.w600, fontFamily: 'Monospace'), + ScaledSizedBox(height: 8), + TextFormField( + key: keyCesiumId, + autofocus: true, + autocorrect: false, + onChanged: (text) { + if (debounce?.isActive ?? false) { + debounce!.cancel(); + } + debounce = Timer(const Duration(milliseconds: debouneTime), () { + if (sub.csSalt.text != '' && sub.csPassword.text != '') { + sub.reload(); + sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); + } + }); + }, + onFieldSubmitted: (text) { + if (sub.csSalt.text != '' && sub.csPassword.text != '') { + if (debounce?.isActive ?? false) { + debounce!.cancel(); + } + sub.reload(); + sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); + } + }, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + controller: sub.csSalt, + obscureText: !sub.isCesiumIDVisible, + style: scaledTextStyle(fontSize: 13), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + hintText: 'enterCesiumId'.tr(), + hintStyle: scaledTextStyle(fontSize: 13), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: IconButton( + key: keyCesiumIdVisible, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: Icon( + sub.isCesiumIDVisible ? Icons.visibility_off : Icons.visibility, + color: Colors.black, + size: scaleSize(18), + ), + onPressed: () { + sub.cesiumIDisVisible(); + }, + ), + ), ), - ), - ], + ScaledSizedBox(height: 8), + TextFormField( + key: keyCesiumPassword, + autofocus: true, + autocorrect: false, + onChanged: (text) { + if (debounce?.isActive ?? false) { + debounce!.cancel(); + } + debounce = Timer(const Duration(milliseconds: debouneTime), () { + sub.g1V1NewAddress = ''; + if (sub.csSalt.text != '' && sub.csPassword.text != '') { + sub.reload(); + sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); + } + }); + }, + onFieldSubmitted: (text) { + if (sub.csSalt.text != '' && sub.csPassword.text != '') { + if (debounce?.isActive ?? false) { + debounce!.cancel(); + } + sub.g1V1NewAddress = ''; + sub.reload(); + sub.csToV2Address(sub.csSalt.text, sub.csPassword.text); + } + }, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.done, + controller: sub.csPassword, + obscureText: !sub.isCesiumIDVisible, + style: scaledTextStyle(fontSize: 13), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + hintText: 'enterCesiumPassword'.tr(), + hintStyle: scaledTextStyle(fontSize: 13), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: Icon( + sub.isCesiumIDVisible ? Icons.visibility_off : Icons.visibility, + color: Colors.black, + size: scaleSize(18), + ), + onPressed: () { + sub.cesiumIDisVisible(); + }, + ), + ), + ), + ], + ), ), - ScaledSizedBox(width: 30), - Column( - children: [ - Text( - '${statusData.fromBalance['transferableBalance']} $unit', - style: scaledTextStyle(fontSize: 15), + ), + + // Section des informations du compte + Visibility( + visible: sub.g1V1OldPubkey != '' && sub.csSalt.text != '' && sub.csPassword.text != '', + child: Card( + elevation: 2, + margin: EdgeInsets.symmetric(vertical: scaleSize(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(scaleSize(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'accountInformation'.tr(), + style: scaledTextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + ScaledSizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + key: keyCopyPubkey, + onTap: () { + Clipboard.setData(ClipboardData(text: sub.g1V1OldPubkey)); + snackCopyKey(context); + }, + child: Row( + children: [ + Text( + 'v1: ', + style: scaledTextStyle(fontSize: 13), + ), + Text( + getShortPubkey(sub.g1V1OldPubkey), + style: scaledTextStyle(fontSize: 13, fontFamily: 'Monospace'), + ), + ScaledSizedBox(width: 6), + Icon(Icons.copy, size: scaleSize(14), color: Colors.grey), + ], + ), + ), + ScaledSizedBox(height: 4), + GestureDetector( + key: keyCopyAddress, + onTap: () { + Clipboard.setData(ClipboardData(text: sub.g1V1NewAddress)); + snackCopyKey(context); + }, + child: Row( + children: [ + Text( + 'v2: ', + style: scaledTextStyle(fontSize: 13), + ), + Text( + getShortPubkey(sub.g1V1NewAddress), + style: scaledTextStyle(fontSize: 13, fontFamily: 'Monospace'), + ), + ScaledSizedBox(width: 6), + Icon(Icons.copy, size: scaleSize(14), color: Colors.grey), + ], + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + BalanceDisplay( + value: statusData.fromBalance['transferableBalance'], + size: 14, + fontWeight: FontWeight.w600, + ), + ScaledSizedBox(height: 4), + Row( + children: [ + IdentityStatus( + address: sub.g1V1NewAddress, + color: Colors.black, + ), + ScaledSizedBox(width: 4), + Certifications( + address: sub.g1V1NewAddress, + size: 12, + ), + ], + ), + ], + ), + ], + ), + ], ), - IdentityStatus(address: sub.g1V1NewAddress, color: Colors.black), - ScaledSizedBox(width: 10), - Certifications(address: sub.g1V1NewAddress, size: 14) - ], - ), - ], - ), - ), - ScaledSizedBox(height: 20), - Text( - 'migrateToThisWallet'.tr(), - style: scaledTextStyle(fontSize: 15), - ), - ScaledSizedBox(height: 5), - DropdownButtonHideUnderline( - key: keySelectWallet, - child: DropdownButton( - value: selectedWallet, - icon: const Icon(Icons.keyboard_arrow_down), - items: myWalletProvider.listWallets.map((wallet) { - return DropdownMenuItem( - key: keySelectThisWallet(wallet.address), - value: wallet, - child: Text( - wallet.name!, - style: scaledTextStyle(fontSize: 15), ), - ); - }).toList(), - onChanged: (WalletData? newSelectedWallet) { - selectedWallet = newSelectedWallet!; - sub.reload(); - }, - ), - ), - ScaledSizedBox(height: 10), - ScaledSizedBox( - width: 320, - height: 50, - child: ElevatedButton( - key: keyConfirm, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: orangeC, - elevation: 2, - padding: const EdgeInsets.symmetric(horizontal: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), ), - shadowColor: orangeC.withValues(alpha: 0.3), ), - onPressed: statusData.canValidate - ? () async { - WalletData? defaultWallet = myWalletProvider.getDefaultWallet(); - String? pin; - if (myWalletProvider.pinCode == '') { - pin = await Navigator.push( - context, - MaterialPageRoute( - builder: (homeContext) { - return UnlockingWallet(wallet: defaultWallet); + // Section de sélection du portefeuille + Card( + elevation: 2, + margin: EdgeInsets.only(bottom: scaleSize(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(scaleSize(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'migrateToThisWallet'.tr(), + style: scaledTextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + ScaledSizedBox(height: 8), + Container( + height: 36, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(horizontal: scaleSize(8)), + child: DropdownButtonHideUnderline( + key: keySelectWallet, + child: DropdownButton( + isExpanded: true, + value: selectedWallet, + icon: const Icon(Icons.keyboard_arrow_down, size: 20), + items: myWalletProvider.listWallets.map((wallet) { + return DropdownMenuItem( + key: keySelectThisWallet(wallet.address), + value: wallet, + child: Text( + wallet.name!, + style: scaledTextStyle(fontSize: 13), + ), + ); + }).toList(), + onChanged: (WalletData? newSelectedWallet) { + selectedWallet = newSelectedWallet!; + sub.reload(); }, ), - ); - } - - final transactionId = await sub.migrateCsToV2( - sub.csSalt.text, - sub.csPassword.text, - selectedWallet.address, - destPassword: pin ?? myWalletProvider.pinCode, - fromBalance: statusData.fromBalance, - fromIdtyStatus: statusData.fromIdtyStatus, - toIdtyStatus: statusData.toIdtyStatus, - ); - Navigator.pop(context); - await Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return TransactionInProgress( - transactionId: transactionId, - transType: 'identityMigration', - fromAddress: getShortPubkey(sub.g1V1NewAddress), - toAddress: getShortPubkey(selectedWallet.address)); - }), - ); - resetScreen(); - } - : null, - child: Text( - 'migrateAccount'.tr(), - style: scaledTextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, + ), + ), + ], + ), ), ), - ), + + // Bouton de validation et message de statut + Column( + children: [ + ScaledSizedBox( + width: double.infinity, + height: 40, + child: ElevatedButton( + key: keyConfirm, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: orangeC, + elevation: 2, + padding: const EdgeInsets.symmetric(horizontal: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + shadowColor: orangeC.withValues(alpha: 0.3), + ), + onPressed: statusData.canValidate + ? () async { + final addressToMigrate = sub.g1V1NewAddress; + final hasIdentity = statusData.fromIdtyStatus != IdtyStatus.none; + final message = hasIdentity + ? 'migrationConfirmWithIdentity'.tr(args: [currencyName, getShortPubkey(selectedWallet.address)]) + : 'migrationConfirmBalanceOnly'.tr(args: [currencyName, getShortPubkey(selectedWallet.address)]); + + // Afficher le popup de confirmation + bool? confirmed = await showConfirmationDialog( + context: context, + title: 'migrationConfirmTitle'.tr(), + message: message, + type: ConfirmationDialogType.info, + ); + + if (confirmed != true) return; + + WalletData? defaultWallet = myWalletProvider.getDefaultWallet(); + + String? pin; + if (myWalletProvider.pinCode == '') { + pin = await Navigator.push( + context, + MaterialPageRoute( + builder: (homeContext) { + return UnlockingWallet(wallet: defaultWallet); + }, + ), + ); + } + + final transactionId = await sub.migrateCsToV2( + sub.csSalt.text, + sub.csPassword.text, + selectedWallet.address, + destPassword: pin ?? myWalletProvider.pinCode, + fromBalance: statusData.fromBalance, + fromIdtyStatus: statusData.fromIdtyStatus, + toIdtyStatus: statusData.toIdtyStatus, + ); + Navigator.pop(context); + await Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return TransactionInProgress( + transactionId: transactionId, + transType: hasIdentity ? 'identityMigration' : 'accountMigration', + fromAddress: getShortPubkey(addressToMigrate), + toAddress: getShortPubkey(selectedWallet.address)); + }), + ); + resetScreen(); + } + : null, + child: Text( + 'migrateAccount'.tr(), + style: scaledTextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ScaledSizedBox(height: 6), + Text( + statusData.validationStatus, + textAlign: TextAlign.center, + style: scaledTextStyle(fontSize: 11, color: Colors.grey[600]), + ), + ], + ), + ], ), - ScaledSizedBox(height: 10), - Text( - statusData.validationStatus, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 11, color: Colors.grey[600]), - ) - ]); + ); }); }), ), diff --git a/lib/screens/myWallets/manage_membership.dart b/lib/screens/myWallets/manage_membership.dart index d56c2d3e0791c451dc269510ddf84fd8fe888e8a..18fb69ee17eb31679b38108f8bbe1498d4cb18a9 100644 --- a/lib/screens/myWallets/manage_membership.dart +++ b/lib/screens/myWallets/manage_membership.dart @@ -9,9 +9,9 @@ import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/utils.dart'; -import 'package:gecko/widgets/commons/common_elements.dart'; import 'package:gecko/screens/myWallets/migrate_identity.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:provider/provider.dart'; import 'package:gecko/models/membership_status.dart'; @@ -145,7 +145,12 @@ class ManageMembership extends StatelessWidget { child: InkWell( key: keyRevokeIdty, onTap: () async { - final answer = await confirmPopup(context, 'areYouSureYouWantToRevokeIdentity'.tr()) ?? false; + final answer = await showConfirmationDialog( + context: context, + message: 'areYouSureYouWantToRevokeIdentity'.tr(), + type: ConfirmationDialogType.warning, + ) ?? + false; if (!answer) return; final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); diff --git a/lib/screens/myWallets/migrate_identity.dart b/lib/screens/myWallets/migrate_identity.dart index c0a39b383fdc7e2653f7a33cd699cce033f3fa16..3497e23e01747d4c3656261ea7874901763413c0 100644 --- a/lib/screens/myWallets/migrate_identity.dart +++ b/lib/screens/myWallets/migrate_identity.dart @@ -15,6 +15,7 @@ import 'package:gecko/providers/wallet_options.dart'; import 'package:gecko/providers/wallets_profiles.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:gecko/utils.dart'; +import 'package:gecko/widgets/balance_display.dart'; import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:polkawallet_sdk/api/apiKeyring.dart'; import 'package:provider/provider.dart'; @@ -24,35 +25,25 @@ class MigrateIdentityScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final walletOptions = - Provider.of<WalletOptionsProvider>(context, listen: false); - final myWalletProvider = - Provider.of<MyWalletsProvider>(context, listen: false); - final generatedWalletsProvider = - Provider.of<GenerateWalletsProvider>(context, listen: false); + final walletOptions = Provider.of<WalletOptionsProvider>(context, listen: false); + final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); + final generatedWalletsProvider = Provider.of<GenerateWalletsProvider>(context, listen: false); final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); final sub = Provider.of<SubstrateSdk>(context, listen: false); + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.height < 700; final fromAddress = walletOptions.address.text; final newMnemonicSentence = TextEditingController(); final newWalletAddress = TextEditingController(); - final mdStyle = MarkdownStyleSheet( - p: scaledTextStyle(fontSize: 15, color: Colors.black, letterSpacing: 0.3), - textAlign: WrapAlignment.center, - ); - final bool isUdUnit = configBox.get('isUdUnit') ?? false; - final unit = isUdUnit ? 'ud'.tr(args: ['']) : currencyName; - var statusData = const MigrateWalletChecks.defaultValues(); var mnemonicIsValid = false; int? matchDerivationNbr; String matchInfo = ''; Future scanDerivations() async { - if (!await isAddress(newWalletAddress.text) || - !await sub.isMnemonicValid(newMnemonicSentence.text) || - !statusData.canValidate) { + if (!await isAddress(newWalletAddress.text) || !await sub.isMnemonicValid(newMnemonicSentence.text) || !statusData.canValidate) { mnemonicIsValid = false; matchInfo = ''; walletOptions.reload(); @@ -75,14 +66,13 @@ class MigrateIdentityScreen extends StatelessWidget { } //Scan derivations - for (int derivationNbr in [ - for (var i = 0; i < generatedWalletsProvider.numberScan; i += 1) i - ]) { + for (int derivationNbr in [for (var i = 0; i < generatedWalletsProvider.numberScan; i += 1) i]) { final addressData = await sub.sdk.api.keyring.addressFromMnemonic( - sub.currencyParameters['ss58']!, - cryptoType: CryptoType.sr25519, - mnemonic: newMnemonicSentence.text, - derivePath: '//$derivationNbr'); + sub.currencyParameters['ss58']!, + cryptoType: CryptoType.sr25519, + mnemonic: newMnemonicSentence.text, + derivePath: '//$derivationNbr', + ); if (addressData.address == newWalletAddress.text) { matchDerivationNbr = derivationNbr; @@ -104,157 +94,319 @@ class MigrateIdentityScreen extends StatelessWidget { backgroundColor: backgroundColor, appBar: GeckoAppBar('migrateIdentity'.tr()), body: SafeArea( - child: Column(children: <Widget>[ - const Row(children: []), - ScaledSizedBox(height: 18), - ScaledSizedBox( - width: 320, - child: MarkdownBody( - data: 'areYouSureMigrateIdentity'.tr(args: [ - duniterIndexer.walletNameIndexer[fromAddress] ?? '???', - '${walletOptions.balanceCache[fromAddress]} $unit' - ]), - styleSheet: mdStyle), - ), - ScaledSizedBox(height: 55), - Text('migrateToThisWallet'.tr(), - style: scaledTextStyle(fontSize: 15)), - ScaledSizedBox(height: 5), - ScaledSizedBox( - width: 320, - child: TextField( - controller: newMnemonicSentence, - autofocus: true, - minLines: 2, - maxLines: 2, - style: scaledTextStyle(fontSize: 13), - decoration: InputDecoration( - icon: Image.asset( - 'assets/onBoarding/phrase_de_restauration_flou.png', - width: scaleSize(30), - ), - hintText: 'enterYourNewMnemonic'.tr(), - hintStyle: scaledTextStyle(fontSize: 13), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: orangeC), - ), - ), - onChanged: (newMnemonic) async { - await scanDerivations(); - }, - ), - ), - ScaledSizedBox(height: 5), - ScaledSizedBox( - width: 320, - child: TextField( - controller: newWalletAddress, - style: scaledTextStyle(fontSize: 13), - decoration: InputDecoration( - icon: Image.asset( - 'assets/walletOptions/key.png', - height: scaleSize(30), - ), - hintText: 'enterYourNewAddress'.tr(args: [currencyName]), - hintStyle: scaledTextStyle(fontSize: 13), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: orangeC), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: scaleSize(24)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + // En-tête avec icône et texte explicatif + Center( + child: Column( + children: [ + Container( + width: scaleSize(isSmallScreen ? 50 : 70), + height: scaleSize(isSmallScreen ? 50 : 70), + decoration: BoxDecoration( + color: orangeC.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.swap_horiz_rounded, + size: scaleSize(isSmallScreen ? 25 : 35), + color: orangeC, + ), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + Wrap( + alignment: WrapAlignment.center, + children: [ + Flexible( + child: MarkdownBody( + data: 'areYouSureMigrateIdentity'.tr(args: [duniterIndexer.walletNameIndexer[fromAddress] ?? '???']), + styleSheet: MarkdownStyleSheet( + p: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: Colors.black87, + height: 1.5, + ), + strong: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: WrapAlignment.center, + ), + ), + ), + BalanceDisplay( + value: walletOptions.balanceCache[fromAddress] ?? 0, + size: isSmallScreen ? 14 : 15, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + Text(' ?', style: scaledTextStyle(fontSize: isSmallScreen ? 14 : 15, color: Colors.black87)), + ], + ), + ], + ), + ), + ScaledSizedBox(height: isSmallScreen ? 24 : 40), + + // Champ de phrase de restauration + Text( + 'migrateToThisWallet'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 15 : 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ScaledSizedBox(height: isSmallScreen ? 12 : 16), + Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: scaleSize(16), + right: scaleSize(16), + top: scaleSize(isSmallScreen ? 8 : 12), + ), + child: Row( + children: [ + Image.asset( + 'assets/onBoarding/phrase_de_restauration_flou.png', + width: scaleSize(isSmallScreen ? 16 : 20), + ), + ScaledSizedBox(width: isSmallScreen ? 8 : 12), + Text( + 'enterYourNewMnemonic'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 13 : 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + TextField( + controller: newMnemonicSentence, + minLines: isSmallScreen ? 2 : 3, + maxLines: isSmallScreen ? 2 : 3, + style: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: Colors.black87, + height: 1.5, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + border: InputBorder.none, + hintText: 'word1 word2 word3 word4 ...', + hintStyle: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: Colors.grey[400], + fontStyle: FontStyle.italic, + ), + ), + onChanged: (newMnemonic) async { + await scanDerivations(); + }, + ), + ], + ), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Champ d'adresse + Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: scaleSize(16), + right: scaleSize(16), + top: scaleSize(isSmallScreen ? 8 : 12), + ), + child: Row( + children: [ + Image.asset( + 'assets/walletOptions/key.png', + width: scaleSize(isSmallScreen ? 16 : 20), + ), + ScaledSizedBox(width: isSmallScreen ? 8 : 12), + Text( + 'enterYourNewAddress'.tr(args: [currencyName]), + style: scaledTextStyle( + fontSize: isSmallScreen ? 13 : 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + TextField( + controller: newWalletAddress, + style: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: Colors.black87, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + border: InputBorder.none, + hintText: 'D....', + hintStyle: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: Colors.grey[400], + fontStyle: FontStyle.italic, + ), + ), + onChanged: (newAddress) async { + if (await isAddress(newAddress)) { + statusData = await sub.getBalanceAndIdtyStatus( + fromAddress, + newAddress, + ); + await scanDerivations(); + } else { + statusData = const MigrateWalletChecks.defaultValues(); + matchInfo = ''; + walletOptions.reload(); + } + }, + ), + ], + ), + ), + ], + ), ), ), - onChanged: (newAddress) async { - if (await isAddress(newAddress)) { - statusData = await sub.getBalanceAndIdtyStatus( - fromAddress, newAddress); - await scanDerivations(); - } else { - statusData = const MigrateWalletChecks.defaultValues(); - matchInfo = ''; - walletOptions.reload(); - } - }, ), - ), - const Spacer(flex: 2), - Consumer<WalletOptionsProvider>(builder: (context, _, __) { - return ScaledSizedBox( - width: 320, - height: 55, - child: ElevatedButton( - key: keyConfirm, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - elevation: 4, - backgroundColor: orangeC, - ), - onPressed: statusData.canValidate && mnemonicIsValid - ? () async { - if (!await myWalletProvider.askPinCode()) return; - - await sub.importAccount( - mnemonic: newMnemonicSentence.text, - derivePath: matchDerivationNbr == -1 - ? '' - : "//$matchDerivationNbr", - password: 'password'); - - final transactionId = await sub.migrateIdentity( - fromAddress: fromAddress, - destAddress: newWalletAddress.text, - fromPassword: myWalletProvider.pinCode, - destPassword: 'password', - withBalance: true, - fromBalance: statusData.fromBalance); - sub.deleteAccounts([newWalletAddress.text]); - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return TransactionInProgress( - transactionId: transactionId, - transType: 'identityMigration', - fromAddress: getShortPubkey(fromAddress), - toAddress: - getShortPubkey(newWalletAddress.text)); - }), - ); - } - : null, - child: Text( - 'migrateIdentity'.tr(), - style: scaledTextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white), - ), + // Messages de statut et bouton de validation + Container( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 16 : 24)), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: Offset(0, -5), + ), + ], ), - ); - }), - Consumer<WalletOptionsProvider>(builder: (context, _, __) { - return ScaledSizedBox( - width: 320, child: Column( + mainAxisSize: MainAxisSize.min, children: [ - ScaledSizedBox(height: 10), - Text( - statusData.validationStatus, - textAlign: TextAlign.center, - style: - scaledTextStyle(fontSize: 11, color: Colors.grey[600]), + Consumer<WalletOptionsProvider>( + builder: (context, _, __) { + return Column( + children: [ + if (statusData.validationStatus.isNotEmpty) + Text( + statusData.validationStatus, + textAlign: TextAlign.center, + style: scaledTextStyle( + fontSize: isSmallScreen ? 12 : 13, + color: Colors.grey[600], + ), + ), + if (matchInfo.isNotEmpty) ...[ + if (statusData.validationStatus.isNotEmpty) ScaledSizedBox(height: isSmallScreen ? 4 : 8), + Text( + matchInfo, + textAlign: TextAlign.center, + style: scaledTextStyle( + fontSize: isSmallScreen ? 12 : 13, + color: Colors.grey[600], + ), + ), + ], + ScaledSizedBox(height: isSmallScreen ? 12 : 16), + ], + ); + }, ), - ScaledSizedBox(height: 5), - Text( - matchInfo, - textAlign: TextAlign.center, - style: - scaledTextStyle(fontSize: 11, color: Colors.grey[600]), + SizedBox( + width: double.infinity, + height: scaleSize(isSmallScreen ? 44 : 50), + child: ElevatedButton( + key: keyConfirm, + style: ElevatedButton.styleFrom( + backgroundColor: orangeC, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: statusData.canValidate && mnemonicIsValid + ? () async { + if (!await myWalletProvider.askPinCode()) return; + + await sub.importAccount( + mnemonic: newMnemonicSentence.text, + derivePath: matchDerivationNbr == -1 ? '' : "//$matchDerivationNbr", + password: 'password', + ); + + final transactionId = await sub.migrateIdentity( + fromAddress: fromAddress, + destAddress: newWalletAddress.text, + fromPassword: myWalletProvider.pinCode, + destPassword: 'password', + withBalance: true, + fromBalance: statusData.fromBalance, + ); + + sub.deleteAccounts([newWalletAddress.text]); + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionInProgress( + transactionId: transactionId, + transType: 'identityMigration', + fromAddress: getShortPubkey(fromAddress), + toAddress: getShortPubkey(newWalletAddress.text), + ), + ), + ); + } + : null, + child: Text( + 'migrateIdentity'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 15 : 16, + fontWeight: FontWeight.w600, + ), + ), + ), ), ], ), - ); - }), - const Spacer(), - ]), + ), + ], + ), ), ); } diff --git a/lib/screens/myWallets/wallet_options.dart b/lib/screens/myWallets/wallet_options.dart index f443814f1d43c21f8277be5e42de814bc5a2406c..b57846ceb12a9036f50f526a898e738863a2dfaf 100644 --- a/lib/screens/myWallets/wallet_options.dart +++ b/lib/screens/myWallets/wallet_options.dart @@ -26,6 +26,7 @@ import 'package:provider/provider.dart'; import 'package:gecko/widgets/buttons/manage_membership_button.dart'; import 'package:gecko/models/membership_renewal.dart'; import 'package:gecko/widgets/wallet_header.dart'; +import 'package:gecko/screens/identity/confirm_identity.dart'; class WalletOptions extends StatelessWidget { const WalletOptions({Key? keyMyWallets, required this.wallet}) : super(key: keyMyWallets); @@ -402,6 +403,7 @@ class WalletOptions extends StatelessWidget { return Visibility( visible: snapshot.hasData && !snapshot.hasError && snapshot.data!.first == IdtyStatus.unconfirmed, child: Column(children: [ + ScaledSizedBox(height: 22), SizedBox( width: double.infinity, height: scaleSize(50), @@ -414,7 +416,16 @@ class WalletOptions extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - onPressed: () => walletProvider.confirmIdentityPopup(context), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConfirmIdentityScreen( + address: walletProvider.address.text, + ), + ), + ); + }, child: Text( 'confirmMyIdentity'.tr(), style: scaledTextStyle(fontSize: 16, color: Colors.white), @@ -425,7 +436,7 @@ class WalletOptions extends StatelessWidget { Text( "someoneCreatedYourIdentity".tr(args: [currencyName]), style: scaledTextStyle( - fontSize: 15, + fontSize: 14, color: Colors.grey[600], fontStyle: FontStyle.italic, ), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index ca10b6676569e7a5c0c0a30cb3a159fa81d36943..f1db96ed68d4e6c9b8fa6834332b79ea35e4ecbc 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -13,61 +13,246 @@ import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:polkawallet_sdk/api/types/networkParams.dart'; import 'package:provider/provider.dart'; -class SettingsScreen extends StatelessWidget { +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State<SettingsScreen> createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State<SettingsScreen> { final MyWalletsProvider _myWallets = MyWalletsProvider(); + final FocusNode _duniterFocusNode = FocusNode(); + final FocusNode _indexerFocusNode = FocusNode(); + late TextEditingController _endpointController; + late TextEditingController _indexerEndpointController; + + @override + void initState() { + super.initState(); + _initControllers(); + } + + void _initControllers() { + final sub = Provider.of<SubstrateSdk>(context, listen: false); + final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); - SettingsScreen({super.key}); + _endpointController = TextEditingController( + text: configBox.containsKey('customEndpoint') ? configBox.get('customEndpoint') : sub.getConnectedEndpoint() ?? 'wss://', + ); + + _indexerEndpointController = TextEditingController( + text: configBox.containsKey('customIndexer') + ? configBox.get('customIndexer') + : duniterIndexer.listIndexerEndpoints.isNotEmpty + ? duniterIndexer.listIndexerEndpoints[0] + : 'https://', + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final sub = Provider.of<SubstrateSdk>(context); + final duniterIndexer = Provider.of<DuniterIndexer>(context); + + // Mise à jour du champ node quand le nœud est connecté + if (sub.nodeConnected && !configBox.containsKey('customEndpoint')) { + final endpoint = sub.getConnectedEndpoint(); + if (endpoint != null && endpoint != _endpointController.text) { + _endpointController.text = endpoint; + } + } + + // Mise à jour du champ indexer quand il devient disponible + if (duniterIndexer.listIndexerEndpoints.isNotEmpty && !configBox.containsKey('customIndexer')) { + final indexerEndpoint = duniterIndexer.listIndexerEndpoints[0]; + if (indexerEndpoint != _indexerEndpointController.text) { + _indexerEndpointController.text = indexerEndpoint; + } + } + } + + @override + void dispose() { + _duniterFocusNode.dispose(); + _indexerFocusNode.dispose(); + _endpointController.dispose(); + _indexerEndpointController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.height < 700; + return Scaffold( backgroundColor: backgroundColor, appBar: GeckoAppBar('parameters'.tr()), - body: Column(children: <Widget>[ - ScaledSizedBox(height: 30), - Text( - 'networkSettings'.tr(), - style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 18), - ), - ScaledSizedBox(height: 20), - duniterEndpointSelection(context), - ScaledSizedBox(height: 30), - indexerEndpointSelection(context), - ScaledSizedBox(height: 35), - Text( - 'displaySettings'.tr(), - style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 18), - ), - ScaledSizedBox(height: 20), - chooseCurrencyUnit(context), - - const Spacer(), - Center( - child: InkWell( - key: keyDeleteAllWallets, - onTap: () async { - log.w('Oublier tous mes coffres'); - await _myWallets.deleteAllWallet(context); - }, - child: ScaledSizedBox( - height: scaleSize(40), - width: 220, - child: Center( - child: Text( - 'forgetAllMyChests'.tr(), + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: scaleSize(24)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Section Réseau + Text( + 'networkSettings'.tr(), style: scaledTextStyle( - fontSize: 16, - color: const Color(0xffD80000), + fontSize: isSmallScreen ? 15 : 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ScaledSizedBox(height: isSmallScreen ? 12 : 16), + + // Carte Nœud Duniter + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: duniterEndpointSelection(context), + ), + ], + ), + ), + ScaledSizedBox(height: isSmallScreen ? 16 : 24), + + // Carte Indexer + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: indexerEndpointSelection(context), + ), + ], + ), + ), + ScaledSizedBox(height: isSmallScreen ? 24 : 32), + + // Section Affichage + Text( + 'displaySettings'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 15 : 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ScaledSizedBox(height: isSmallScreen ? 12 : 16), + + // Carte Unité de devise + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: chooseCurrencyUnit(context), + ), + ], + ), + ), + ScaledSizedBox(height: isSmallScreen ? 24 : 32), + + // Section Danger + Text( + 'dangerZone'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 15 : 16, fontWeight: FontWeight.w600, + color: const Color(0xffD80000), ), ), - ), + ScaledSizedBox(height: isSmallScreen ? 12 : 16), + + // Carte Suppression des coffres + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xffD80000).withValues(alpha: 0.1)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + key: keyDeleteAllWallets, + onTap: () async { + log.w('Oublier tous mes coffres'); + await _myWallets.deleteAllWallet(context); + }, + child: Padding( + padding: EdgeInsets.all(scaleSize(isSmallScreen ? 12 : 16)), + child: Row( + children: [ + Icon( + Icons.delete_forever_rounded, + color: const Color(0xffD80000), + size: scaleSize(isSmallScreen ? 20 : 24), + ), + ScaledSizedBox(width: 12), + Text( + 'forgetAllMyChests'.tr(), + style: scaledTextStyle( + fontSize: isSmallScreen ? 14 : 15, + color: const Color(0xffD80000), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ScaledSizedBox(height: isSmallScreen ? 24 : 32), + ], ), ), ), - // const Spacer(), - ScaledSizedBox(height: 70), - ]), + ), ); } @@ -78,36 +263,119 @@ class SettingsScreen extends StatelessWidget { onTap: () async { await homeProvider.changeCurrencyUnit(context); }, - child: ScaledSizedBox( - height: 50, - child: Row( - children: [ - ScaledSizedBox(width: 12), - Text('showUdAmounts'.tr(), style: scaledTextStyle(fontSize: 14)), - const Spacer(), - Consumer<HomeProvider>(builder: (context, homeProvider, _) { + child: Row( + children: [ + Icon( + Icons.calculate_rounded, + color: orangeC, + size: scaleSize(24), + ), + ScaledSizedBox(width: 12), + Text( + 'showUdAmounts'.tr(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + const Spacer(), + Consumer<HomeProvider>( + builder: (context, homeProvider, _) { final bool isUdUnit = configBox.get('isUdUnit') ?? false; - return Icon( - isUdUnit ? Icons.check_box : Icons.check_box_outline_blank, - color: orangeC, - size: scaleSize(27), + return Switch( + value: isUdUnit, + activeColor: orangeC, + inactiveThumbColor: Colors.grey[400], + inactiveTrackColor: Colors.grey[300], + onChanged: (bool value) async { + await homeProvider.changeCurrencyUnit(context); + }, ); - }), - ScaledSizedBox(width: 30), - ], - ), + }, + ), + ], ), ); } + Future<void> _showNodeSelectionDialog(BuildContext context, List<NetworkParams> nodes, String selectedEndpoint, TextEditingController controller) async { + final sub = Provider.of<SubstrateSdk>(context, listen: false); + final set = Provider.of<SettingsProvider>(context, listen: false); + + String? result = await showDialog<String>( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'selectNode'.tr(), + style: scaledTextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: nodes.map((node) { + final isSelected = node.endpoint == selectedEndpoint; + return InkWell( + onTap: () { + Navigator.of(context).pop(node.endpoint); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: scaleSize(12), + horizontal: scaleSize(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: isSelected ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Expanded( + child: Text( + node.endpoint!, + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + contentPadding: EdgeInsets.symmetric(vertical: scaleSize(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ); + }, + ); + + if (result != null) { + controller.text = result; + configBox.put('autoEndpoint', false); + configBox.put('customEndpoint', result); + await sub.connectNode(); + set.reload(); + } + } + Widget duniterEndpointSelection(BuildContext context) { final sub = Provider.of<SubstrateSdk>(context, listen: false); String? selectedDuniterEndpoint; - // List of items in our dropdown menu var duniterBootstrapNodes = sub.getDuniterBootstrap(); - selectedDuniterEndpoint = - sub.getConnectedEndpoint() ?? duniterBootstrapNodes.first.endpoint; + selectedDuniterEndpoint = sub.getConnectedEndpoint() ?? duniterBootstrapNodes.first.endpoint; final customEndpoint = NetworkParams(); customEndpoint.endpoint = 'Personnalisé'; @@ -115,7 +383,6 @@ class SettingsScreen extends StatelessWidget { localEndpoint.endpoint = 'ws://10.0.2.2:9944'; final automaticEndpoint = NetworkParams(); automaticEndpoint.endpoint = 'Auto'; - // duniterBootstrapNodes.add(_sub.getDuniterCustomEndpoint()); duniterBootstrapNodes.insert(0, automaticEndpoint); duniterBootstrapNodes.add(localEndpoint); duniterBootstrapNodes.add(customEndpoint); @@ -123,147 +390,333 @@ class SettingsScreen extends StatelessWidget { if (configBox.get('autoEndpoint') == true) { selectedDuniterEndpoint = automaticEndpoint.endpoint; } else if (configBox.containsKey('customEndpoint')) { - selectedDuniterEndpoint = customEndpoint.endpoint; + selectedDuniterEndpoint = configBox.get('customEndpoint'); } - final endpointController = TextEditingController( - text: configBox.containsKey('customEndpoint') - ? configBox.get('customEndpoint') - : 'wss://'); - - return Column(children: <Widget>[ - Row(children: [ - Consumer<SubstrateSdk>(builder: (context, sub, _) { - return Expanded( - child: Row(children: [ - ScaledSizedBox(width: 2), - ScaledSizedBox( - width: 55, - child: Text( - 'currencyNode'.tr(), - style: scaledTextStyle(fontSize: 14), - ), - ), - const Spacer(), - ScaledSizedBox( - width: 30, - child: Icon(sub.nodeConnected && !sub.isLoadingEndpoint - ? Icons.check - : Icons.close), - ), - if (sub.nodeConnected && !sub.isLoadingEndpoint) - const Icon(Icons.add_card_sharp, size: 0.01), - const Spacer(), - ScaledSizedBox( - height: 52, - width: 230, - child: Consumer<SettingsProvider>(builder: (context, set, _) { - return DropdownButtonHideUnderline( - key: keySelectDuniterNodeDropDown, - child: DropdownButton( - style: scaledTextStyle(fontSize: 14, color: Colors.black), - value: selectedDuniterEndpoint, - icon: const Icon(Icons.keyboard_arrow_down), - items: duniterBootstrapNodes - .map((NetworkParams endpointParams) { - return DropdownMenuItem( - key: keySelectDuniterNode(endpointParams.endpoint!), - value: endpointParams.endpoint, - child: Text(endpointParams.endpoint!), + final endpointController = _endpointController; + + String getDisplayMode() { + if (configBox.get('autoEndpoint') == true) return 'Auto'; + if (selectedDuniterEndpoint == 'Personnalisé') return 'Manuel'; + return 'Manuel'; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer<SubstrateSdk>( + builder: (context, sub, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.dns_rounded, + color: orangeC, + size: scaleSize(24), + ), + ScaledSizedBox(width: 12), + Text( + 'currencyNode'.tr(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ScaledSizedBox(width: 12), + Icon( + sub.nodeConnected && !sub.isLoadingEndpoint ? Icons.check_circle : Icons.error, + color: sub.nodeConnected && !sub.isLoadingEndpoint ? Colors.green : Colors.red, + size: scaleSize(16), + ), + const Spacer(), + Consumer<SettingsProvider>( + builder: (context, set, _) { + return PopupMenuButton<String>( + key: keySelectDuniterNodeDropDown, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: scaleSize(12), + vertical: scaleSize(6), + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getDisplayMode(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ScaledSizedBox(width: 4), + Icon( + Icons.arrow_drop_down, + color: Colors.grey[600], + size: scaleSize(20), + ), + ], + ), + ), + itemBuilder: (context) => [ + PopupMenuItem( + key: keySelectDuniterNode('Auto'), + value: 'Auto', + child: Row( + children: [ + Icon( + configBox.get('autoEndpoint') == true ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: configBox.get('autoEndpoint') == true ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'Auto', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + PopupMenuItem( + key: keySelectDuniterNode('manual'.tr()), + value: 'manual'.tr(), + child: Row( + children: [ + Icon( + configBox.get('autoEndpoint') != true ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: configBox.get('autoEndpoint') != true ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'manual'.tr(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + PopupMenuItem( + key: keySelectDuniterNode('select'.tr()), + value: 'select'.tr(), + child: Row( + children: [ + Icon( + Icons.list_alt, + color: Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'select'.tr(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + ], + onSelected: (String value) async { + if (value == 'select'.tr()) { + await _showNodeSelectionDialog( + context, + duniterBootstrapNodes + .where((node) => node.endpoint != 'Auto' && node.endpoint != 'Personnalisé' && node.endpoint != 'ws://10.0.2.2:9944') + .toList(), + selectedDuniterEndpoint ?? '', + endpointController, + ); + } else if (value == 'Auto') { + configBox.delete('customEndpoint'); + configBox.put('autoEndpoint', true); + await sub.connectNode(); + set.reload(); + } else { + configBox.put('autoEndpoint', false); + if (!configBox.containsKey('customEndpoint')) { + configBox.put('customEndpoint', _endpointController.text); + } + set.reload(); + _duniterFocusNode.requestFocus(); + _endpointController.selection = TextSelection.fromPosition( + TextPosition(offset: _endpointController.text.length), + ); + } + }, ); - }).toList(), - onChanged: (String? newEndpoint) { - selectedDuniterEndpoint = newEndpoint; - set.reload(); }, ), - ); - }), - ), - const Spacer(flex: 3), - sub.isLoadingEndpoint - ? Loading(size: scaleSize(32), stroke: 2.5) - : Consumer<SettingsProvider>(builder: (context, set, _) { - return IconButton( - key: keyConnectToEndpoint, - icon: Icon( - Icons.send, - color: selectedDuniterEndpoint != - sub.getConnectedEndpoint() - ? orangeC - : Colors.grey[500], - size: scaleSize(35), - ), - onPressed: selectedDuniterEndpoint != - sub.getConnectedEndpoint() - ? () async { - if (selectedDuniterEndpoint == 'Auto') { - configBox.delete('customEndpoint'); - configBox.put('autoEndpoint', true); - } else { - configBox.put('autoEndpoint', false); - final finalEndpoint = - selectedDuniterEndpoint == - 'Personnalisé' - ? endpointController.text - : selectedDuniterEndpoint; - configBox.put( - 'customEndpoint', finalEndpoint); - } - await sub.connectNode(); - } - : null); - }), - const Spacer(flex: 8), - ]), - ); - }), - ]), - Consumer<SettingsProvider>(builder: (context, set, _) { - return Visibility( - visible: selectedDuniterEndpoint == 'Personnalisé', - child: ScaledSizedBox( - width: 200, - height: 50, - child: TextField( - key: keyCustomDuniterEndpoint, - controller: endpointController, - autocorrect: false, - style: scaledTextStyle(fontSize: 14), - ), - ), - ); - }), - Consumer<SubstrateSdk>(builder: (context, sub, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Consumer<SettingsProvider>(builder: (context, set, _) { - return Visibility( - visible: selectedDuniterEndpoint == 'Auto', - child: ScaledSizedBox( - width: 250, - height: sub.getConnectedEndpoint() == null ? 60 : 20, - child: Text( + ], + ), + if (sub.isLoadingEndpoint) + Padding( + padding: EdgeInsets.only(top: scaleSize(16)), + child: Center(child: Loading(size: scaleSize(24), stroke: 2)), + ), + ], + ); + }, + ), + Consumer<SettingsProvider>( + builder: (context, set, _) { + if (configBox.get('autoEndpoint') == true) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: 8), + Text( sub.getConnectedEndpoint() ?? "anAutoNodeChoosed".tr(), style: scaledTextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: Colors.grey[600], + ), + ), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: TextField( + key: keyCustomDuniterEndpoint, + focusNode: _duniterFocusNode, + controller: endpointController, + autocorrect: false, + style: scaledTextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + horizontal: scaleSize(12), + vertical: scaleSize(8), + ), + border: InputBorder.none, + hintText: 'wss://', + hintStyle: scaledTextStyle( fontSize: 14, - fontStyle: FontStyle.italic, - color: Colors.grey[700]!), + color: Colors.grey[400], + ), + ), + onSubmitted: (value) async { + configBox.put('customEndpoint', value); + await sub.connectNode(); + set.reload(); + }, ), ), - ); - }), - Text( - 'blockN'.tr(args: [ - sub.blocNumber.toString() - ]), //'bloc N°${sub.blocNumber}', - style: scaledTextStyle(fontSize: 13, color: Colors.grey[700]), - ) - ], + ], + ); + }, + ), + Consumer<SubstrateSdk>( + builder: (context, sub, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: 8), + Text( + 'blockN'.tr(args: [sub.blocNumber.toString()]), + style: scaledTextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ); + }, + ), + ], + ); + } + + Future<void> _showIndexerSelectionDialog(BuildContext context, List<String> indexers, String selectedEndpoint, TextEditingController controller) async { + final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); + final set = Provider.of<SettingsProvider>(context, listen: false); + + String? result = await showDialog<String>( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'selectIndexer'.tr(), + style: scaledTextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: indexers.map((endpoint) { + final isSelected = endpoint == selectedEndpoint; + return InkWell( + onTap: () { + Navigator.of(context).pop(endpoint); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: scaleSize(12), + horizontal: scaleSize(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: isSelected ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Expanded( + child: Text( + endpoint, + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + contentPadding: EdgeInsets.symmetric(vertical: scaleSize(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ); - }), - ]); + }, + ); + + if (result != null) { + controller.text = result; + configBox.put('customIndexer', result); + await duniterIndexer.checkIndexerEndpoint(result); + set.reload(); + } } Widget indexerEndpointSelection(BuildContext context) { @@ -271,124 +724,238 @@ class SettingsScreen extends StatelessWidget { String? selectedIndexerEndpoint; if (configBox.containsKey('customIndexer')) { - selectedIndexerEndpoint = 'Personnalisé'; + selectedIndexerEndpoint = configBox.get('customIndexer'); } else { - selectedIndexerEndpoint = indexerEndpoint; + selectedIndexerEndpoint = duniterIndexer.listIndexerEndpoints.isNotEmpty ? duniterIndexer.listIndexerEndpoints[0] : 'https://'; } - if (selectedIndexerEndpoint == '') { - selectedIndexerEndpoint = duniterIndexer.listIndexerEndpoints[0]; + final indexerEndpointController = _indexerEndpointController; + + String getDisplayMode() { + return configBox.containsKey('customIndexer') ? 'Manuel' : 'Auto'; } - final indexerEndpointController = TextEditingController( - text: configBox.containsKey('customIndexer') - ? configBox.get('customIndexer') - : 'https://'); - - return Column(children: <Widget>[ - Row(children: [ - Consumer<DuniterIndexer>(builder: (context, indexer, _) { - return Expanded( - child: Row(children: [ - ScaledSizedBox(width: 5), - ScaledSizedBox( - width: 55, - child: Text('Indexer', style: scaledTextStyle(fontSize: 14)), - ), - const Spacer(), - Icon(indexerEndpoint != '' ? Icons.check : Icons.close), - const Spacer(), - ScaledSizedBox( - width: 230, - child: Consumer<SettingsProvider>(builder: (context, set, _) { - return DropdownButtonHideUnderline( - child: DropdownButton( - style: scaledTextStyle(fontSize: 14, color: Colors.black), - value: selectedIndexerEndpoint, - icon: const Icon(Icons.keyboard_arrow_down), - items: - indexer.listIndexerEndpoints.map((indexerEndpoint) { - return DropdownMenuItem( - value: indexerEndpoint, - child: Text(indexerEndpoint), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer<DuniterIndexer>( + builder: (context, indexer, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.storage_rounded, + color: orangeC, + size: scaleSize(24), + ), + ScaledSizedBox(width: 12), + Text( + 'Indexer', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ScaledSizedBox(width: 12), + Icon( + indexerEndpoint != '' ? Icons.check_circle : Icons.error, + color: indexerEndpoint != '' ? Colors.green : Colors.red, + size: scaleSize(16), + ), + const Spacer(), + Consumer<SettingsProvider>( + builder: (context, set, _) { + return PopupMenuButton<String>( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: scaleSize(12), + vertical: scaleSize(6), + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getDisplayMode(), + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ScaledSizedBox(width: 4), + Icon( + Icons.arrow_drop_down, + color: Colors.grey[600], + size: scaleSize(20), + ), + ], + ), + ), + itemBuilder: (context) => [ + PopupMenuItem( + value: 'Auto', + child: Row( + children: [ + Icon( + !configBox.containsKey('customIndexer') ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: !configBox.containsKey('customIndexer') ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'Auto', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'Manuel', + child: Row( + children: [ + Icon( + configBox.containsKey('customIndexer') ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: configBox.containsKey('customIndexer') ? orangeC : Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'Manuel', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'Sélectionner', + child: Row( + children: [ + Icon( + Icons.list_alt, + color: Colors.grey[400], + size: scaleSize(20), + ), + ScaledSizedBox(width: 12), + Text( + 'Sélectionner', + style: scaledTextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + ], + onSelected: (String value) async { + if (value == 'Sélectionner') { + await _showIndexerSelectionDialog( + context, + indexer.listIndexerEndpoints.cast<String>(), + selectedIndexerEndpoint ?? '', + indexerEndpointController, + ); + } else if (value == 'Auto') { + configBox.delete('customIndexer'); + final defaultEndpoint = duniterIndexer.listIndexerEndpoints.isNotEmpty ? duniterIndexer.listIndexerEndpoints[0] : 'https://'; + selectedIndexerEndpoint = defaultEndpoint; + indexerEndpointController.text = defaultEndpoint; + await indexer.checkIndexerEndpoint(defaultEndpoint); + set.reload(); + } else { + if (!configBox.containsKey('customIndexer')) { + configBox.put('customIndexer', _indexerEndpointController.text); + } + set.reload(); + _indexerFocusNode.requestFocus(); + _indexerEndpointController.selection = TextSelection.fromPosition( + TextPosition(offset: _indexerEndpointController.text.length), + ); + } + }, ); - }).toList(), - onChanged: (newEndpoint) { - selectedIndexerEndpoint = newEndpoint.toString(); - set.reload(); }, ), - ); - }), - ), - const Spacer(flex: 5), - indexer.isLoadingIndexer - ? Loading(size: scaleSize(32), stroke: 2.5) - : Consumer<SettingsProvider>(builder: (context, set, _) { - return IconButton( - icon: Icon( - Icons.send, - color: selectedIndexerEndpoint != indexerEndpoint - ? orangeC - : Colors.grey[500], - size: scaleSize(35), - ), - onPressed: selectedIndexerEndpoint != indexerEndpoint - ? () async { - final finalEndpoint = - selectedIndexerEndpoint == 'Personnalisé' - ? indexerEndpointController.text - : selectedIndexerEndpoint!; - - if (selectedIndexerEndpoint == - 'Personnalisé') { - configBox.put('customIndexer', - indexerEndpointController.text); - } else { - configBox.delete('customIndexer'); - } - await indexer - .checkIndexerEndpoint(finalEndpoint); - } - : null); - }), - const Spacer(flex: 8), - ]), - ); - }), - ]), - Consumer<SettingsProvider>(builder: (context, set, _) { - return Visibility( - visible: selectedIndexerEndpoint == 'Personnalisé', - child: ScaledSizedBox( - width: 200, - height: 50, - child: TextField( - controller: indexerEndpointController, - autocorrect: false, - style: scaledTextStyle(fontSize: 14), - ), - ), - ); - }), - Consumer<SubstrateSdk>(builder: (context, sub, _) { - return Consumer<SettingsProvider>(builder: (context, set, _) { - return Visibility( - visible: selectedIndexerEndpoint == 'Auto', - child: ScaledSizedBox( - width: 250, - height: 60, - child: Text( - sub.getConnectedEndpoint() ?? "anAutoNodeChoosed".tr(), - style: scaledTextStyle( - fontSize: 14, - fontStyle: FontStyle.italic, - color: Colors.grey[700]), - ), - ), - ); - }); - }), - ]); + ], + ), + if (indexer.isLoadingIndexer) + Padding( + padding: EdgeInsets.only(top: scaleSize(16)), + child: Center(child: Loading(size: scaleSize(24), stroke: 2)), + ), + ], + ); + }, + ), + Consumer<SettingsProvider>( + builder: (context, set, _) { + if (!configBox.containsKey('customIndexer')) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: 8), + Text( + selectedIndexerEndpoint ?? '', + style: scaledTextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: Colors.grey[600], + ), + ), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ScaledSizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: TextField( + focusNode: _indexerFocusNode, + controller: indexerEndpointController, + autocorrect: false, + style: scaledTextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + horizontal: scaleSize(12), + vertical: scaleSize(8), + ), + border: InputBorder.none, + hintText: 'https://', + hintStyle: scaledTextStyle( + fontSize: 14, + color: Colors.grey[400], + ), + ), + onSubmitted: (value) async { + configBox.put('customIndexer', value); + await duniterIndexer.checkIndexerEndpoint(value); + set.reload(); + }, + ), + ), + ], + ); + }, + ), + ], + ); } } diff --git a/lib/screens/wallet_view.dart b/lib/screens/wallet_view.dart index 78dc874f843e33a2b7f868ea8d6faf02cd67b553..00e3a6521d2ec3714a23c7479f4ce54b4046ca2d 100644 --- a/lib/screens/wallet_view.dart +++ b/lib/screens/wallet_view.dart @@ -22,18 +22,32 @@ import 'package:gecko/widgets/commons/wallet_app_bar.dart'; const double buttonSize = 75; const double buttonFontSize = 13; -class WalletViewScreen extends StatelessWidget { +class WalletViewScreen extends StatefulWidget { const WalletViewScreen({required this.address, required this.username, super.key}); final String address; final String? username; + @override + State<WalletViewScreen> createState() => _WalletViewScreenState(); +} + +class _WalletViewScreenState extends State<WalletViewScreen> { + late String address; + late String? username; + + @override + void initState() { + super.initState(); + address = widget.address; + username = widget.username; + } + @override Widget build(BuildContext context) { final walletProfile = Provider.of<WalletsProfilesProvider>(context, listen: false); final sub = Provider.of<SubstrateSdk>(context, listen: false); final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); final defaultWallet = myWalletProvider.getDefaultWallet(); - // final wallet = myWalletProvider.getWalletDataByAddress(address)!; walletProfile.address = address; sub.setCurrentWallet(defaultWallet); diff --git a/lib/widgets/certifications.dart b/lib/widgets/certifications.dart index 6f6030a89799dd5c3e5f5cb0dbc1aeb57075b5c8..cbce6997d98c786f329c68c067bb315364676ebc 100644 --- a/lib/widgets/certifications.dart +++ b/lib/widgets/certifications.dart @@ -4,46 +4,46 @@ import 'package:gecko/providers/substrate_sdk.dart'; import 'package:provider/provider.dart'; class Certifications extends StatelessWidget { - const Certifications( - {super.key, - required this.address, - required this.size, - this.color = Colors.black}); + const Certifications({super.key, required this.address, required this.size, this.color = Colors.black}); final String address; final double size; final Color color; @override Widget build(BuildContext context) { - final sub = Provider.of<SubstrateSdk>(context); + final sub = Provider.of<SubstrateSdk>(context, listen: true); - return Column(children: <Widget>[ - FutureBuilder( - future: sub.getCertsCounter(address), - builder: (BuildContext context, AsyncSnapshot<List<int>?> certs) { - if ((certs.data == null || certs.data!.isEmpty) || - sub.certsCounterCache[address] == null) { - return const SizedBox.shrink(); - } + // Si on a les données en cache, on les affiche directement + final cachedCerts = sub.certsCounterCache[address]; + if (cachedCerts != null && cachedCerts.isNotEmpty) { + return _buildContent(cachedCerts[0], cachedCerts[1]); + } - final receivedCount = sub.certsCounterCache[address]![0]; - final sentCount = sub.certsCounterCache[address]![1]; + // Sinon on utilise un FutureBuilder pour charger les données + return FutureBuilder( + future: sub.getCertsCounter(address), + builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) { + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const SizedBox.shrink(); + } - return Row( - children: [ - Image.asset('assets/medal.png', - color: color, height: scaleSize(18)), - ScaledSizedBox(width: 1), - Text(receivedCount.toString(), - style: scaledTextStyle(fontSize: size, color: color)), - ScaledSizedBox(width: 5), - Text( - "($sentCount)", - style: scaledTextStyle(fontSize: size * 0.7, color: color), - ) - ], - ); - }), - ]); + return _buildContent(snapshot.data![0], snapshot.data![1]); + }, + ); + } + + Widget _buildContent(int receivedCount, int sentCount) { + return Row( + children: [ + Image.asset('assets/medal.png', color: color, height: scaleSize(18)), + ScaledSizedBox(width: 1), + Text(receivedCount.toString(), style: scaledTextStyle(fontSize: size, color: color)), + ScaledSizedBox(width: 5), + Text( + "($sentCount)", + style: scaledTextStyle(fontSize: size * 0.7, color: color), + ) + ], + ); } } diff --git a/lib/widgets/certify/certify_button.dart b/lib/widgets/certify/certify_button.dart index c0a77b1501e79f243f2f049e967457069849712c..1193953ad1e62af555d696a5be068b0184a60a2d 100644 --- a/lib/widgets/certify/certify_button.dart +++ b/lib/widgets/certify/certify_button.dart @@ -13,7 +13,7 @@ import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:gecko/screens/wallet_view.dart' show buttonSize, buttonFontSize; import 'package:gecko/utils.dart'; -import 'package:gecko/widgets/commons/common_elements.dart'; +import 'package:gecko/widgets/commons/confirmation_dialog.dart'; import 'package:provider/provider.dart'; class CertifyButton extends StatelessWidget { @@ -37,12 +37,16 @@ class CertifyButton extends StatelessWidget { key: keyCertify, splashColor: orangeC, onTap: () async { - final result = await confirmPopupCertification( - context, - 'areYouSureYouWantToCertify1'.tr(), - duniterIndexer.walletNameIndexer[address] ?? "noIdentity".tr(), - 'areYouSureYouWantToCertify2'.tr(), - getShortPubkey(address), + final walletName = duniterIndexer.walletNameIndexer[address]; + final message = walletName != null + ? '${'areYouSureYouWantToCertify1'.tr()}\n\n**$walletName**\n\n${'areYouSureYouWantToCertify2'.tr()}\n\n**${getShortPubkey(address)}**' + : '${'areYouSureCreateIdentityOnAddress'.tr()}\n\n**${getShortPubkey(address)}**'; + + final result = await showConfirmationDialog( + context: context, + title: walletName != null ? 'certification'.tr() : 'identityCreation'.tr(), + message: message, + type: walletName != null ? ConfirmationDialogType.question : ConfirmationDialogType.info, ) ?? false; diff --git a/lib/widgets/commons/common_elements.dart b/lib/widgets/commons/common_elements.dart index 4ad8d1c04b08de6406132a4cf75b9773ba738b8a..6108530859bd3d79a05827c4aac38116fa41f4e6 100644 --- a/lib/widgets/commons/common_elements.dart +++ b/lib/widgets/commons/common_elements.dart @@ -4,134 +4,6 @@ import 'package:gecko/globals.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/widgets_keys.dart'; -Future<bool?> confirmPopup(BuildContext context, String title) async { - return showDialog<bool>( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: backgroundColor, - content: Text( - title, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), - actions: <Widget>[ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - key: keyConfirm, - child: Text( - "yes".tr(), - style: scaledTextStyle( - fontSize: 18, - color: const Color(0xffD80000), - ), - ), - onPressed: () { - Navigator.pop(context, true); - }, - ), - ScaledSizedBox(width: 20), - TextButton( - child: Text( - "no".tr(), - style: scaledTextStyle(fontSize: 18, color: Colors.blueAccent), - ), - onPressed: () { - Navigator.pop(context, false); - }, - ), - ScaledSizedBox(height: 70) - ], - ) - ], - ); - }, - ); -} - -Future<bool?> confirmPopupCertification(BuildContext context, String question1, String username, String question2, String address) async { - return showDialog<bool>( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: backgroundColor, - content: ScaledSizedBox( - height: 220, - child: Column( - children: [ - ScaledSizedBox(height: 10), - Text( - question1, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 16, fontWeight: FontWeight.w400), - ), - ScaledSizedBox(height: 15), - Text( - username, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 19, fontWeight: FontWeight.w500), - ), - ScaledSizedBox(height: 15), - Text( - question2, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 16, fontWeight: FontWeight.w400), - ), - ScaledSizedBox(height: 15), - Text( - address, - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 16, fontWeight: FontWeight.w500), - ), - ScaledSizedBox(height: 15), - Text( - '?', - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 16, fontWeight: FontWeight.w400), - ), - ], - ), - ), - actions: <Widget>[ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - key: keyConfirm, - child: Text( - "yes".tr(), - style: scaledTextStyle( - fontSize: 18, - color: const Color(0xffD80000), - ), - ), - onPressed: () { - Navigator.pop(context, true); - }, - ), - ScaledSizedBox(width: 32), - TextButton( - child: Text( - "no".tr(), - style: scaledTextStyle(fontSize: 18), - ), - onPressed: () { - Navigator.pop(context, false); - }, - ), - ScaledSizedBox(height: 120) - ], - ) - ], - ); - }, - ); -} - Future<void> infoPopup(BuildContext context, String title) async { return showDialog<void>( context: context, diff --git a/lib/widgets/commons/confirmation_dialog.dart b/lib/widgets/commons/confirmation_dialog.dart new file mode 100644 index 0000000000000000000000000000000000000000..2d6b7bee4e3080653fbdcd4c81ce71708bd99ae0 --- /dev/null +++ b/lib/widgets/commons/confirmation_dialog.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:gecko/globals.dart'; +import 'package:gecko/models/scale_functions.dart'; + +/// Type de message pour le dialogue de confirmation +enum ConfirmationDialogType { + info, + warning, + success, + error, + question, +} + +/// Extension pour obtenir l'icône correspondante au type de message +extension ConfirmationDialogTypeExtension on ConfirmationDialogType { + IconData get icon => switch (this) { + ConfirmationDialogType.info => Icons.info_rounded, + ConfirmationDialogType.warning => Icons.warning_rounded, + ConfirmationDialogType.success => Icons.task_alt_rounded, + ConfirmationDialogType.error => Icons.error_rounded, + ConfirmationDialogType.question => Icons.help_rounded, + }; + + Color get iconColor => switch (this) { + ConfirmationDialogType.info => orangeC, + ConfirmationDialogType.warning => const Color(0xFFFF9800), + ConfirmationDialogType.success => const Color(0xFF4CAF50), + ConfirmationDialogType.error => const Color(0xFFF44336), + ConfirmationDialogType.question => const Color(0xFF673AB7), + }; +} + +Future<bool?> showConfirmationDialog({ + required BuildContext context, + String? title, + required String message, + String? cancelText, + String? confirmText, + bool barrierDismissible = true, + ConfirmationDialogType type = ConfirmationDialogType.info, + IconData? customIcon, + Color? customIconColor, +}) { + final IconData iconToShow = customIcon ?? type.icon; + final Color iconColorToShow = customIconColor ?? type.iconColor; + final String dialogTitle = title ?? 'confirmationTitle'.tr(); + + return showDialog<bool>( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: iconColorToShow.withValues(alpha: 0.1), + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: iconColorToShow.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + iconToShow, + color: iconColorToShow, + size: 32, + ), + ), + SizedBox(height: 20), + Text( + dialogTitle, + style: scaledTextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16), + Column( + mainAxisSize: MainAxisSize.min, + children: message.split('\n\n').map((text) { + final bool isBold = text.startsWith('**') && text.endsWith('**'); + final String cleanText = isBold ? text.substring(2, text.length - 2) : text; + + return Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + cleanText, + style: scaledTextStyle( + fontSize: 15, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + ); + }).toList(), + ), + SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + cancelText ?? 'cancel'.tr(), + style: scaledTextStyle( + fontSize: 15, + color: Colors.grey[600], + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: iconColorToShow, + foregroundColor: Colors.white, + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + confirmText ?? 'confirm'.tr(), + style: scaledTextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); +} diff --git a/lib/widgets/commons/text_markdown.dart b/lib/widgets/commons/text_markdown.dart new file mode 100644 index 0000000000000000000000000000000000000000..9cb94a8b7c75e2852684a2815970625293d14fda --- /dev/null +++ b/lib/widgets/commons/text_markdown.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:url_launcher/url_launcher.dart'; + +class TextMarkDown extends StatelessWidget { + const TextMarkDown( + this.data, { + super.key, + this.style, + this.textAlign, + this.markdownStyle, + this.selectable = false, + this.styleSheetTheme, + this.syntaxHighlighter, + this.onSelectionChanged, + this.onTapLink, + this.onTapText, + this.imageDirectory, + this.blockSyntaxes, + this.inlineSyntaxes, + this.extensionSet, + this.imageBuilder, + this.checkboxBuilder, + this.bulletBuilder, + this.builders = const <String, MarkdownElementBuilder>{}, + this.paddingBuilders = const <String, MarkdownPaddingBuilder>{}, + this.listItemCrossAxisAlignment = MarkdownListItemCrossAxisAlignment.baseline, + this.shrinkWrap = true, + this.fitContent = true, + this.softLineBreak = false, + this.canUnderline = false, + }); + + final String data; + final TextStyle? style; + final MarkdownStyleSheet? markdownStyle; + final WrapAlignment? textAlign; + final bool selectable; + final MarkdownStyleSheetBaseTheme? styleSheetTheme; + final SyntaxHighlighter? syntaxHighlighter; + final void Function(String?, TextSelection, SelectionChangedCause?)? onSelectionChanged; + final void Function(String, String?, String)? onTapLink; + final void Function()? onTapText; + final String? imageDirectory; + final List<md.BlockSyntax>? blockSyntaxes; + final List<md.InlineSyntax>? inlineSyntaxes; + final md.ExtensionSet? extensionSet; + final Widget Function(Uri, String?, String?)? imageBuilder; + final Widget Function(bool)? checkboxBuilder; + final Widget Function(MarkdownBulletParameters)? bulletBuilder; + final Map<String, MarkdownElementBuilder> builders; + final Map<String, MarkdownPaddingBuilder> paddingBuilders; + final MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment; + final bool shrinkWrap; + final bool fitContent; + final bool softLineBreak; + final bool canUnderline; + + @override + Widget build(BuildContext context) { + final markdownStyleSheet = markdownStyle ?? + MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + p: style, + textAlign: textAlign, + a: style?.copyWith( + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + em: style?.copyWith( + fontStyle: canUnderline ? style!.fontStyle : FontStyle.italic, + decoration: canUnderline ? TextDecoration.underline : style!.decoration, + ), + ); + return MarkdownBody( + data: data, + selectable: selectable, + styleSheet: markdownStyleSheet, + styleSheetTheme: styleSheetTheme, + syntaxHighlighter: syntaxHighlighter, + onSelectionChanged: onSelectionChanged, + onTapLink: onTapLink ?? + (text, url, title) { + launchUrl(Uri.parse(url ?? 'https://helios.do')); + }, + onTapText: onTapText, + imageDirectory: imageDirectory, + blockSyntaxes: blockSyntaxes, + inlineSyntaxes: inlineSyntaxes, + extensionSet: extensionSet, + imageBuilder: imageBuilder, + checkboxBuilder: checkboxBuilder, + bulletBuilder: bulletBuilder, + builders: builders, + paddingBuilders: paddingBuilders, + listItemCrossAxisAlignment: listItemCrossAxisAlignment, + shrinkWrap: shrinkWrap, + fitContent: fitContent, + softLineBreak: softLineBreak, + ); + } +} diff --git a/lib/widgets/transaction_status.dart b/lib/widgets/transaction_status.dart index 6cce929bdb905297f53a82ee4587309578a99c96..01da62f954b6177360cd3c4392c149366e9465da 100644 --- a/lib/widgets/transaction_status.dart +++ b/lib/widgets/transaction_status.dart @@ -9,6 +9,7 @@ Map<String, String> actionMap = { 'revokeIdty': 'revokeAdhesion'.tr(), 'identityMigration': 'identityMigration'.tr(), 'renewMembership': 'renewingMembership'.tr(), + 'accountMigration': 'accountMigration'.tr(), }; Map<TransactionStatus, String> statusStatusMap = { diff --git a/lib/widgets/transaction_tile.dart b/lib/widgets/transaction_tile.dart index 077c2dd791d88cc4729ba614dc3182d4b00ad94c..7482a229196e60bd564c8019b2715d182262a70e 100644 --- a/lib/widgets/transaction_tile.dart +++ b/lib/widgets/transaction_tile.dart @@ -133,7 +133,7 @@ class TransactionTile extends StatelessWidget { ), trailing: BalanceDisplay( value: finalAmount, - size: scaleSize(15), + size: scaleSize(13), color: transaction.isReceived ? const Color(0xFF4CAF50) : const Color(0xFF2196F3), fontWeight: FontWeight.w500, ), diff --git a/lib/widgets/wallet_header.dart b/lib/widgets/wallet_header.dart index c2286aabae7f38af298193c49996ae5d4b27d6af..2e179af7c2e839d2a2cd8f8b7d367e566cf9ba77 100644 --- a/lib/widgets/wallet_header.dart +++ b/lib/widgets/wallet_header.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/my_wallets.dart'; @@ -16,8 +17,10 @@ import 'package:gecko/widgets/idty_status.dart'; import 'package:gecko/widgets/page_route_no_transition.dart'; import 'package:provider/provider.dart'; import 'package:gecko/providers/wallet_options.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; +import 'package:gecko/models/wallet_header_data.dart'; -class WalletHeader extends StatelessWidget { +class WalletHeader extends StatefulWidget { const WalletHeader({ super.key, required this.address, @@ -30,17 +33,87 @@ class WalletHeader extends StatelessWidget { final String? defaultImagePath; @override - Widget build(BuildContext context) { - const double avatarSize = 90; + State<WalletHeader> createState() => _WalletHeaderState(); +} + +class _WalletHeaderState extends State<WalletHeader> { + late Future<WalletHeaderData> _loadData; + static final Map<String, WalletHeaderData> _cache = {}; + bool _isPickerOpen = false; + String _newCustomImagePath = ''; + + @override + void initState() { + super.initState(); + _loadData = _initializeData(); + } + + Future<WalletHeaderData> _initializeData() async { + // Vérifie d'abord le cache + final cached = _cache[widget.address]; + if (cached != null) { + // Rafraîchit en arrière-plan + _refreshData(); + return cached; + } + + final sub = Provider.of<SubstrateSdk>(context, listen: false); final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); - final walletData = myWalletProvider.getWalletDataByAddress(address); - final isOwner = walletData != null; + // Charge toutes les données en parallèle + final results = await Future.wait([ + sub.idtyStatus(widget.address), + sub.getBalance(widget.address), + sub.getCertsCounter(widget.address), + ]); + + final data = WalletHeaderData( + hasIdentity: results[0] != IdtyStatus.none, + isOwner: myWalletProvider.isOwner(widget.address), + walletName: duniterIndexer.walletNameIndexer[widget.address], + balance: BigInt.from((results[1] as Map<String, int>)['transferableBalance'] ?? 0), + certCount: (results[2] as List<int>?) ?? [0, 0], + ); + + _cache[widget.address] = data; + return data; + } - bool isPickerOpen = false; - String newCustomImagePath = ''; + Future<void> _refreshData() async { + if (!mounted) return; + final sub = Provider.of<SubstrateSdk>(context, listen: false); + final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); + final myWalletProvider = Provider.of<MyWalletsProvider>(context, listen: false); + + final results = await Future.wait([ + sub.idtyStatus(widget.address), + sub.getBalance(widget.address), + sub.getCertsCounter(widget.address), + ]); + + final data = WalletHeaderData( + hasIdentity: results[0] != IdtyStatus.none, + isOwner: myWalletProvider.isOwner(widget.address), + walletName: duniterIndexer.walletNameIndexer[widget.address], + balance: BigInt.from((results[1] as Map<String, int>)['transferableBalance'] ?? 0), + certCount: (results[2] as List<int>?) ?? [0, 0], + ); + + final existing = _cache[widget.address]; + if (existing == null || !existing.equals(data)) { + _cache[widget.address] = data; + if (mounted) { + setState(() { + _loadData = Future.value(data); + }); + } + } + } + + Widget _buildContent(BuildContext context, bool hasIdentity, bool isOwner, bool isPickerOpen, String newCustomImagePath, DuniterIndexer duniterIndexer) { + const double avatarSize = 90; return Container( color: headerColor, padding: EdgeInsets.only( @@ -57,7 +130,7 @@ class WalletHeader extends StatelessWidget { height: scaleSize(90), decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.white, + color: Colors.white.withValues(alpha: 25), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), @@ -68,37 +141,40 @@ class WalletHeader extends StatelessWidget { ), child: Consumer<WalletOptionsProvider>( builder: (context, walletOptionsProvider, child) { - if (newCustomImagePath.isEmpty) { - newCustomImagePath = customImagePath ?? ''; + if (_newCustomImagePath.isEmpty) { + _newCustomImagePath = widget.customImagePath ?? ''; } return Stack( children: [ Material( color: Colors.transparent, child: InkWell( - onTap: isOwner && !isPickerOpen + onTap: isOwner && !_isPickerOpen ? () async { - isPickerOpen = true; + setState(() => _isPickerOpen = true); walletOptionsProvider.reload(); - newCustomImagePath = await walletOptionsProvider.changeAvatar(); - isPickerOpen = false; + final newPath = await walletOptionsProvider.changeAvatar(); + setState(() { + _newCustomImagePath = newPath; + _isPickerOpen = false; + }); walletOptionsProvider.reload(); } : null, customBorder: const CircleBorder(), child: ClipOval( - child: newCustomImagePath.isEmpty - ? (defaultImagePath != null + child: _newCustomImagePath.isEmpty + ? (widget.defaultImagePath != null ? Image.asset( - 'assets/avatars/$defaultImagePath', + 'assets/avatars/${widget.defaultImagePath}', fit: BoxFit.cover, ) : DatapodAvatar( - address: address, + address: widget.address, size: avatarSize, )) : Image.file( - File(newCustomImagePath), + File(_newCustomImagePath), fit: BoxFit.cover, ), ), @@ -118,12 +194,15 @@ class WalletHeader extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: !isPickerOpen + onTap: !_isPickerOpen ? () async { - isPickerOpen = true; + setState(() => _isPickerOpen = true); walletOptionsProvider.reload(); - newCustomImagePath = await walletOptionsProvider.changeAvatar(); - isPickerOpen = false; + final newPath = await walletOptionsProvider.changeAvatar(); + setState(() { + _newCustomImagePath = newPath; + _isPickerOpen = false; + }); walletOptionsProvider.reload(); } : null, @@ -154,13 +233,13 @@ class WalletHeader extends StatelessWidget { GestureDetector( key: keyCopyAddress, onTap: () { - Clipboard.setData(ClipboardData(text: address)); + Clipboard.setData(ClipboardData(text: widget.address)); snackCopyKey(context); }, child: Row( children: [ Text( - getShortPubkey(address), + getShortPubkey(widget.address), style: scaledTextStyle( fontSize: 20, fontFamily: 'Monospace', @@ -177,7 +256,7 @@ class WalletHeader extends StatelessWidget { color: orangeC.withValues(alpha: 0.5), ), onPressed: () { - Clipboard.setData(ClipboardData(text: address)); + Clipboard.setData(ClipboardData(text: widget.address)); snackCopyKey(context); }, ), @@ -187,19 +266,19 @@ class WalletHeader extends StatelessWidget { ScaledSizedBox(height: 8), // Balance - Balance(address: address, size: 18), + Balance(address: widget.address, size: 18), // Certifications section ScaledSizedBox(height: 12), Visibility( - visible: walletData?.hasIdentity ?? false, + visible: hasIdentity, child: InkWell( onTap: () => Navigator.push( context, PageNoTransit( builder: (context) => CertificationsScreen( - address: address, - username: duniterIndexer.walletNameIndexer[address] ?? '', + address: widget.address, + username: duniterIndexer.walletNameIndexer[widget.address] ?? '', ), ), ), @@ -212,12 +291,12 @@ class WalletHeader extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ IdentityStatus( - address: address, + address: widget.address, color: orangeC, ), SizedBox(width: scaleSize(8)), Certifications( - address: address, + address: widget.address, size: 13, ), Icon( @@ -237,4 +316,156 @@ class WalletHeader extends StatelessWidget { ), ); } + + Widget _buildLoadingHeader() { + const double avatarSize = 90; + return Container( + color: headerColor, + padding: EdgeInsets.only( + left: scaleSize(16), + right: scaleSize(16), + bottom: scaleSize(16), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Avatar placeholder + Container( + width: scaleSize(avatarSize), + height: scaleSize(avatarSize), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.1), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + ), + SizedBox(width: scaleSize(20)), + + // Info section placeholders + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Address placeholder + Row( + children: [ + Container( + width: scaleSize(150), + height: scaleSize(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + SizedBox(width: scaleSize(14)), + Container( + width: scaleSize(20), + height: scaleSize(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + ], + ), + ScaledSizedBox(height: 8), + + // Balance placeholder + Container( + width: scaleSize(120), + height: scaleSize(18), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + + // Certifications placeholder + ScaledSizedBox(height: 12), + Row( + children: [ + Container( + width: scaleSize(20), + height: scaleSize(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + SizedBox(width: scaleSize(8)), + Container( + width: scaleSize(80), + height: scaleSize(13), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + Container( + width: scaleSize(15), + height: scaleSize(15), + margin: EdgeInsets.only(left: scaleSize(4)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white.withValues(alpha: 0.1), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: false); + + // Si les données sont en cache, on les affiche immédiatement + final cached = _cache[widget.address]; + if (cached != null) { + return _buildContent( + context, + cached.hasIdentity, + cached.isOwner, + _isPickerOpen, + _newCustomImagePath, + duniterIndexer, + ); + } + + // Sinon on affiche le loading + return FutureBuilder<WalletHeaderData>( + future: _loadData, + builder: (context, AsyncSnapshot<WalletHeaderData> snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildLoadingHeader(); + } + + if (snapshot.hasError || !snapshot.hasData) { + return const SizedBox.shrink(); + } + + final data = snapshot.data!; + return _buildContent( + context, + data.hasIdentity, + data.isOwner, + _isPickerOpen, + _newCustomImagePath, + duniterIndexer, + ); + }, + ); + } } diff --git a/pubspec.lock b/pubspec.lock index 7687bced2a1de17898f7cec3fcff9fa3b8adaccf..df611b76a99f9b5863fb75dceb4858f394c6792d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1011,7 +1011,7 @@ packages: source: hosted version: "0.1.3-main.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 diff --git a/pubspec.yaml b/pubspec.yaml index 067656a56fa5687a92d99e1e4db76ba904b1d5e6..9c097f7a74d7dde5d906ab032b5ef85a0fe61b25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: screen_brightness: ^2.0.1 uuid: ^4.5.1 fade_and_translate: ^0.1.3 + markdown: ^7.2.2 # durt2: # path: ../../durt2 # git: