From 85e79fe3a8f24f85b8f1fa30f49020e6f6e66b28 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 30 Apr 2025 15:36:27 +0300 Subject: [PATCH 1/6] Edit farm item to be a card instead of a dropdown, add farm details page w node cards section --- app/lib/helpers/country_code.dart | 194 ++++++++++ app/lib/models/farm.dart | 4 + app/lib/screens/farm_details.dart | 565 ++++++++++++++++++++++++++++ app/lib/screens/farm_screen.dart | 21 +- app/lib/widgets/farm_item.dart | 395 ++++--------------- app/lib/widgets/farm_node_item.dart | 216 ++++++++--- app/pubspec.lock | 16 + app/pubspec.yaml | 1 + 8 files changed, 1045 insertions(+), 367 deletions(-) create mode 100644 app/lib/helpers/country_code.dart create mode 100644 app/lib/screens/farm_details.dart diff --git a/app/lib/helpers/country_code.dart b/app/lib/helpers/country_code.dart new file mode 100644 index 000000000..91987550d --- /dev/null +++ b/app/lib/helpers/country_code.dart @@ -0,0 +1,194 @@ +final Map countryNameToCode = { + 'Afghanistan': 'AF', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Brazil': 'BR', + 'Brunei': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cabo Verde': 'CV', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Costa Rica': 'CR', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Eswatini': 'SZ', + 'Ethiopia': 'ET', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Greece': 'GR', + 'Grenada': 'GD', + 'Guatemala': 'GT', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Honduras': 'HN', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Israel': 'IL', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + 'Laos': 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mexico': 'MX', + 'Micronesia': 'FM', + 'Moldova': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'North Korea': 'KP', + 'North Macedonia': 'MK', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestine State': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Qatar': 'QA', + 'Romania': 'RO', + 'Russia': 'RU', + 'Rwanda': 'RW', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Korea': 'KR', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syria': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', + }; \ No newline at end of file diff --git a/app/lib/models/farm.dart b/app/lib/models/farm.dart index 1bcecd923..7cdc729bb 100644 --- a/app/lib/models/farm.dart +++ b/app/lib/models/farm.dart @@ -4,9 +4,13 @@ class Node { Node({ required this.nodeId, required this.status, + required this.country, + this.uptime, }); final int nodeId; final NodeStatus status; + final int? uptime; + final String? country; } class Farm { diff --git a/app/lib/screens/farm_details.dart b/app/lib/screens/farm_details.dart new file mode 100644 index 000000000..52ca623ac --- /dev/null +++ b/app/lib/screens/farm_details.dart @@ -0,0 +1,565 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/models/farm.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/wallets/contacts.dart'; +import 'package:threebotlogin/services/stellar_service.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:threebotlogin/widgets/farm_node_item.dart'; +import 'package:registrar_client/registrar_client.dart' as registrar; + +class FarmDetails extends StatefulWidget { + const FarmDetails({ + super.key, + required this.farm, + required this.wallets, + required this.isV4, + }); + + final Farm farm; + final List wallets; + final bool isV4; + + @override + _FarmDetailsState createState() => _FarmDetailsState(); +} + +class _FarmDetailsState extends State { + final walletAddressController = TextEditingController(); + + bool showTfchainSecret = false; + bool editStellarAddress = false; + bool isSavingStellarAddress = false; + + final walletAddressFocus = FocusNode(); + + String? stellarAddressError; + String? currentStellarAddress; + + @override + void initState() { + super.initState(); + currentStellarAddress = widget.farm.walletAddress; + walletAddressController.text = currentStellarAddress!; + } + + @override + void dispose() { + walletAddressController.dispose(); + walletAddressFocus.dispose(); + super.dispose(); + } + + Future _validateStellarAddress(String address) async { + setState(() { + stellarAddressError = null; + }); + + if (address.isEmpty) { + setState(() { + stellarAddressError = 'Address cannot be empty'; + }); + return; + } + + if (!isValidStellarAddress(address)) { + setState(() { + stellarAddressError = 'Invalid Stellar address format'; + }); + return; + } + + try { + final balance = await getBalanceByAccountId(address); + if (balance == '-1') { + setState(() { + stellarAddressError = 'Wallet not activated on stellar'; + }); + } + } catch (e) { + logger.e('Error fetching account balance for validation: $e'); + if (stellarAddressError == null) { + setState(() { + stellarAddressError = 'Error validating address'; + }); + } + } + } + + Future _saveStellarPayoutAddress() async { + if (stellarAddressError != null || + walletAddressController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(stellarAddressError ?? 'Address cannot be empty', + style: TextStyle( + color: Theme.of(context).colorScheme.errorContainer)))); + return; + } + + setState(() { + isSavingStellarAddress = true; + }); + + final String newAddress = walletAddressController.text.trim(); + if (newAddress == currentStellarAddress) { + if (context.mounted) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('No changes to save.'))); + } + setState(() { + isSavingStellarAddress = false; + editStellarAddress = false; + }); + return; + } + + try { + final balance = await getBalanceByAccountId(newAddress); + if (balance == '-1') { + if (context.mounted) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Wallet not activated on stellar', + style: TextStyle( + color: Theme.of(context).colorScheme.errorContainer)))); + } + setState(() { + isSavingStellarAddress = false; + }); + return; + } + + if (widget.isV4) { + final client = registrar.RegistrarClient( + baseUrl: Globals().registrarURL, + mnemonicOrSeed: widget.farm.tfchainWalletSecret); + await client.farms.update(widget.farm.twinId, widget.farm.farmId, + stellarAddress: newAddress); + } else { + await addStellarAddress( + widget.farm.tfchainWalletSecret, + widget.farm.farmId, + newAddress, + ); + } + + if (context.mounted) { + final savingAddressSuccess = SnackBar( + content: Text( + 'Address is saved Successfully.', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(savingAddressSuccess); + } + + setState(() { + currentStellarAddress = newAddress; + editStellarAddress = false; + }); + } catch (e) { + logger.e('Failed to update stellar address due to $e'); + if (context.mounted) { + final savingAddressFailure = SnackBar( + content: Text( + 'Failed to Update Stellar address', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.errorContainer, + ), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(savingAddressFailure); + } + } finally { + setState(() { + isSavingStellarAddress = false; + }); + } + } + + void _cancelStellarAddressEdit() { + setState(() { + editStellarAddress = false; + walletAddressController.text = currentStellarAddress!; + stellarAddressError = null; + }); + walletAddressFocus.unfocus(); + } + + void _selectAddress(String address) { + setState(() { + walletAddressController.text = address; + _validateStellarAddress(address.trim()); + }); + Navigator.pop(context); + } + + Widget _buildDisplayField({ + required String label, + required String value, + Widget? trailing, + bool obscureText = false, + TextEditingController? controller, + }) { + final displayController = controller ?? TextEditingController(text: value); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Row( + children: [ + Expanded( + child: TextField( + controller: displayController, + readOnly: true, + obscureText: obscureText, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + if (trailing != null) trailing, + ], + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.farm.name), + ), + body: KeyboardVisibilityBuilder( + builder: (context, isKeyboardVisible) { + return GestureDetector( + onTap: () { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + }, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDisplayField( + label: 'Farm ID', + value: widget.farm.farmId.toString(), + ), + const Divider(height: 20, thickness: 1), + _buildDisplayField( + label: 'Twin ID', + value: widget.farm.twinId.toString(), + ), + const Divider(height: 20, thickness: 1), + _buildDisplayField( + label: 'Wallet Name', + value: widget.farm.walletName, + ), + const Divider(height: 20, thickness: 1), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + 'Stellar Payout Address', + style: + Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + focusNode: walletAddressFocus, + autofocus: editStellarAddress, + readOnly: !editStellarAddress, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + overflow: editStellarAddress + ? TextOverflow.clip + : TextOverflow.ellipsis, + ), + controller: walletAddressController, + onChanged: (value) { + if (editStellarAddress) { + _validateStellarAddress(value.trim()); + } + }, + decoration: InputDecoration( + hintText: editStellarAddress + ? 'Enter Stellar address' + : '', + isDense: true, + contentPadding: + const EdgeInsets.symmetric(vertical: 8.0), + border: InputBorder.none, + suffixIcon: editStellarAddress + ? IconButton( + onPressed: () { + Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => ContactsScreen( + chainType: ChainType.Stellar, + currentWalletAddress: + walletAddressController + .text, + wallets: widget.wallets + .where((w) => + double.tryParse(w + .stellarBalance) != + null && + double.parse(w + .stellarBalance) >= + 0) + .toList(), + onSelectToAddress: + _selectAddress), + )); + }, + icon: const Icon(Icons.person), + tooltip: 'Select from Wallets', + ) + : null, + )), + ), + isSavingStellarAddress + ? Transform.scale( + scale: 0.5, + child: const CircularProgressIndicator()) + : editStellarAddress + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: stellarAddressError == + null && + walletAddressController.text + .trim() + .isNotEmpty + ? _saveStellarPayoutAddress + : null, + icon: Icon( + Icons.save, + color: stellarAddressError == + null && + walletAddressController.text + .trim() + .isNotEmpty + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + tooltip: 'Save Changes', + ), + IconButton( + onPressed: _cancelStellarAddressEdit, + icon: + const Icon(Icons.cancel_outlined), + tooltip: 'Cancel Editing', + ), + ], + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () { + setState(() { + editStellarAddress = true; + }); + walletAddressFocus.requestFocus(); + walletAddressController.selection = + TextSelection( + baseOffset: 0, + extentOffset: + walletAddressController + .text.length); + }, + icon: const Icon(Icons.edit), + tooltip: 'Edit Address', + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData( + text: walletAddressController + .text)); + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy), + tooltip: 'Copy Address', + ), + ], + ), + ], + ), + if (stellarAddressError != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + stellarAddressError!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + Text( + 'This address will be used for payout.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + const Divider(height: 20, thickness: 1), + if (!widget.isV4) ...[ + _buildDisplayField( + label: 'TFChain Secret', + value: widget.farm.tfchainWalletSecret, + obscureText: !showTfchainSecret, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () { + setState(() { + showTfchainSecret = !showTfchainSecret; + }); + }, + icon: Icon(showTfchainSecret + ? Icons.visibility + : Icons.visibility_off), + tooltip: showTfchainSecret + ? 'Hide Secret' + : 'Show Secret', + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData( + text: widget.farm.tfchainWalletSecret)); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy), + tooltip: 'Copy Secret', + ), + ], + ), + ), + Text( + 'Use this secret to log in to the ThreeFold Dashboard.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const Divider(height: 20, thickness: 1), + ], + if (widget.farm.nodes.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'Nodes (${widget.farm.nodes.length})', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.farm.nodes.length, + itemBuilder: (context, index) { + final node = widget.farm.nodes[index]; + return Card( + margin: const EdgeInsets.symmetric( + vertical: 4.0, horizontal: 0.0), + elevation: 1.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + side: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0, + ), + ), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.only(top: 6.0, bottom: 6.0), // Increased top padding + child: FarmNodeItemWidget( + node: node, + isV4: widget.isV4, + farmName: widget.farm.name, + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index 8059b3b5e..266942fd2 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -111,11 +111,15 @@ class _FarmScreenState extends ConsumerState farmId: farm.farmID, nodes: nodes .map((node) => Node( - nodeId: node.nodeId, - status: NodeStatus.values.firstWhere( - (e) => - e.toString().toLowerCase() == 'nodestatus.${node.status}', - ))) + nodeId: node.nodeId, + status: NodeStatus.values.firstWhere( + (e) => + e.toString().toLowerCase() == + 'nodestatus.${node.status}', + ), + country: node.location!.country, + uptime: node.uptime, + )) .toList(), ); }).toList()); @@ -144,7 +148,12 @@ class _FarmScreenState extends ConsumerState farmId: f.farmID!, nodes: nodes .where((n) => n.farmID == f.farmID) - .map((n) => Node(nodeId: n.nodeID, status: NodeStatus.Up)) + .map((n) => Node( + nodeId: n.nodeID, + status: NodeStatus.Up, + country: n.location.country, + uptime: + (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0)) .toList(), ))); } catch (e) { diff --git a/app/lib/widgets/farm_item.dart b/app/lib/widgets/farm_item.dart index bfa1f6a6a..4889f01a6 100644 --- a/app/lib/widgets/farm_item.dart +++ b/app/lib/widgets/farm_item.dart @@ -1,22 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:threebotlogin/helpers/globals.dart'; -import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/wallet.dart'; -import 'package:threebotlogin/screens/wallets/contacts.dart'; -import 'package:threebotlogin/services/stellar_service.dart'; -import 'package:threebotlogin/services/tfchain_service.dart'; -import 'package:threebotlogin/widgets/farm_node_item.dart'; -import 'package:registrar_client/registrar_client.dart' as registrar; +import 'package:threebotlogin/screens/farm_details.dart'; class FarmItemWidget extends StatefulWidget { - const FarmItemWidget( - {super.key, - required this.farm, - required this.wallets, - required this.isV4}); + const FarmItemWidget({ + super.key, + required this.farm, + required this.wallets, + required this.isV4, + }); + final Farm farm; final List wallets; final bool isV4; @@ -31,11 +25,14 @@ class _FarmItemWidgetState extends State { final walletNameController = TextEditingController(); final twinIdController = TextEditingController(); final farmIdController = TextEditingController(); + bool showTfchainSecret = false; bool edit = false; bool isSaving = false; + final walletFocus = FocusNode(); ChainType chainType = ChainType.Stellar; + String? addressError; String? currentAddress; @@ -44,6 +41,11 @@ class _FarmItemWidgetState extends State { super.initState(); currentAddress = widget.farm.walletAddress; walletAddressController.text = currentAddress!; + + tfchainWalletSecretController.text = widget.farm.tfchainWalletSecret; + walletNameController.text = widget.farm.walletName; + farmIdController.text = widget.farm.farmId.toString(); + twinIdController.text = widget.farm.twinId.toString(); } @override @@ -53,313 +55,82 @@ class _FarmItemWidgetState extends State { walletNameController.dispose(); twinIdController.dispose(); farmIdController.dispose(); + walletFocus.dispose(); super.dispose(); } - - _editStellarPayoutAddress() async { - setState(() { - isSaving = true; - }); - - final String newAddress = walletAddressController.text.trim(); - if (newAddress == currentAddress) { - FocusScope.of(context).requestFocus(walletFocus); - setState(() { - isSaving = false; - edit = false; - }); - return; - } - - try { - if (widget.isV4) { - final client = registrar.RegistrarClient( - baseUrl: Globals().registrarURL, - mnemonicOrSeed: widget.farm.tfchainWalletSecret); - await client.farms.update(widget.farm.twinId, widget.farm.farmId, - stellarAddress: newAddress); - } else { - await addStellarAddress( - widget.farm.tfchainWalletSecret, - widget.farm.farmId, - newAddress, - ); - } - final savingAddressSuccess = SnackBar( - content: Text( - 'Address is saved Successfully.', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - ), - duration: const Duration(seconds: 3), - ); - setState(() { - currentAddress = newAddress; - }); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(savingAddressSuccess); - } catch (e) { - logger.e('Failed to add stellar address due to $e'); - if (context.mounted) { - final savingAddressFailure = SnackBar( - content: Text( - 'Failed to Add Stellar address', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.errorContainer, - ), - ), - duration: const Duration(seconds: 3), - ); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(savingAddressFailure); - } - } finally { - setState(() { - edit = false; - isSaving = false; - }); - } - } - - void validateStellarAddress(String address) async { - setState(() { - addressError = - isValidStellarAddress(address) ? null : 'Invalid Stellar address'; - }); - - if (addressError == null) { - try { - final balance = await getBalanceByAccountId(address); - if (balance == '-1') { - setState(() { - addressError = 'Wallet not activated on stellar'; - }); - } - } catch (e) { - setState(() { - addressError = 'Error fetching account balance'; - }); - } - } - } - - void _selectAddress(String address) { - setState(() { - walletAddressController.text = address; - }); - } - - void _cancelEdit() { - setState(() { - edit = false; - walletAddressController.text = currentAddress!; - addressError = null; - }); - } - + @override Widget build(BuildContext context) { - tfchainWalletSecretController.text = widget.farm.tfchainWalletSecret; - walletNameController.text = widget.farm.walletName; - farmIdController.text = widget.farm.farmId.toString(); - twinIdController.text = widget.farm.twinId.toString(); - - return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - FocusScope.of(context).unfocus(); - }, - child: ExpansionTile( - title: Text( - widget.farm.name, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FarmDetails( + farm: widget.farm, + wallets: widget.wallets, + isV4: widget.isV4, ), - childrenPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + ), + ); + }, + child: Card( + margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + elevation: 2.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + clipBehavior: Clip.antiAlias, + color: Colors.grey[850], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - ListTile( - title: TextField( - focusNode: walletFocus, - autofocus: edit, - readOnly: !edit, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - controller: walletAddressController, - onChanged: (value) { - validateStellarAddress(value.trim()); - }, - decoration: InputDecoration( - errorText: addressError, - labelText: 'Stellar Payout Address', - suffixIcon: edit - ? IconButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ContactsScreen( - chainType: chainType, - currentWalletAddress: currentAddress!, - wallets: widget.wallets - .where((w) => - double.parse( - w.stellarBalance) >= - 0) - .toList(), - onSelectToAddress: _selectAddress), - )); - }, - icon: const Icon(Icons.person)) - : null)), - subtitle: const Text('This address will be used for payout.'), - trailing: isSaving - ? Transform.scale( - scale: 0.5, child: const CircularProgressIndicator()) - : edit - ? SizedBox( - width: 100, - child: Row( - children: [ - IconButton( - onPressed: addressError == null - ? () { - _editStellarPayoutAddress(); - } - : null, - icon: Icon( - Icons.save, - color: addressError == null - ? Theme.of(context) - .colorScheme - .onSurface - : Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - IconButton( - onPressed: _cancelEdit, - icon: const Icon( - Icons.cancel_outlined, - ), - ) - ], - ), - ) - : SizedBox( - width: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - setState(() { - edit = !edit; - }); - if (edit) { - FocusScope.of(context) - .requestFocus(walletFocus); - } - }, - icon: edit - ? const Icon(Icons.save) - : const Icon(Icons.edit)), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData( - text: walletAddressController.text)); - ScaffoldMessenger.of(context) - .clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy), - ), - ], - ), - ), - ), - if (!widget.isV4) - ListTile( - title: TextField( - readOnly: true, - obscureText: !showTfchainSecret, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - controller: tfchainWalletSecretController, - decoration: InputDecoration( - labelText: 'TFChain Secret', - suffixIcon: IconButton( - onPressed: () { - setState(() { - showTfchainSecret = !showTfchainSecret; - }); - }, - icon: Icon(showTfchainSecret - ? Icons.visibility - : Icons.visibility_off)), - )), - subtitle: const Text( - 'Use this secret to log in to the ThreeFold Dashboard.'), - trailing: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData( - text: tfchainWalletSecretController.text)); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy)), + const Padding( + padding: EdgeInsets.only(right: 16.0), + child: Icon( + Icons.menu, + color: Colors.white70, ), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - controller: walletNameController, - decoration: const InputDecoration( - labelText: 'Wallet Name', - )), ), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - controller: twinIdController, - decoration: const InputDecoration( - labelText: 'Twin ID', - )), - ), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - controller: farmIdController, - decoration: const InputDecoration( - labelText: 'Farm ID', - )), - ), - if (widget.farm.nodes.isNotEmpty) - ExpansionTile( - title: const Text('Nodes'), - childrenPadding: const EdgeInsets.only(left: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - for (final node in widget.farm.nodes) - FarmNodeItemWidget(node: node, isV4: widget.isV4) + Text( + widget.farm.name, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (widget.farm.nodes.isNotEmpty) + const SizedBox(height: 4.0), + if (widget.farm.nodes.isNotEmpty) + Text( + '${widget.farm.nodes.length} Node${widget.farm.nodes.length == 1 ? '' : 's'}', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.white70, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ], - ) + ), + ), ], - )); - }); + ), + ), + ), + ); } -} +} \ No newline at end of file diff --git a/app/lib/widgets/farm_node_item.dart b/app/lib/widgets/farm_node_item.dart index 49b65f844..31f508ecd 100644 --- a/app/lib/widgets/farm_node_item.dart +++ b/app/lib/widgets/farm_node_item.dart @@ -1,73 +1,191 @@ import 'package:flutter/material.dart'; -import 'package:threebotlogin/main.dart'; +import 'package:threebotlogin/helpers/country_code.dart'; import 'package:threebotlogin/models/farm.dart'; +import 'package:flag/flag.dart'; class FarmNodeItemWidget extends StatefulWidget { - const FarmNodeItemWidget({super.key, required this.node, required this.isV4}); + const FarmNodeItemWidget({ + super.key, + required this.node, + required this.isV4, + required this.farmName, + }); + final Node node; final bool isV4; + final String farmName; @override State createState() => _FarmNodeItemWidgetState(); } class _FarmNodeItemWidgetState extends State { - final nodeIdController = TextEditingController(); - @override void dispose() { - nodeIdController.dispose(); super.dispose(); } - @override - Widget build(BuildContext context) { - nodeIdController.text = widget.node.nodeId.toString(); - - final Color statusColor; - if (widget.node.status == NodeStatus.Up) { - statusColor = Theme.of(context).colorScheme.primaryContainer; - } else if (widget.node.status == NodeStatus.Down) { - statusColor = Theme.of(context).colorScheme.errorContainer; - } else { - statusColor = Theme.of(context).colorScheme.warningContainer; + String _formatUptime(int totalSeconds) { + if (totalSeconds <= 0) { + return '0s'; } - final Color statusTextColor; - if (widget.node.status == NodeStatus.Up) { - statusTextColor = Theme.of(context).colorScheme.onPrimaryContainer; - } else if (widget.node.status == NodeStatus.Down) { - statusTextColor = Theme.of(context).colorScheme.onErrorContainer; - } else { - statusTextColor = Theme.of(context).colorScheme.onWarningContainer; + + final int secondsPerMinute = 60; + final int secondsPerHour = 60 * secondsPerMinute; + final int secondsPerDay = 24 * secondsPerHour; + + int remainingSeconds = totalSeconds; + final List parts = []; + + // Calculate and add days + final int days = remainingSeconds ~/ secondsPerDay; + if (days > 0) { + parts.add('${days}d'); + remainingSeconds %= secondsPerDay; + } + + // Calculate and add hours + final int hours = remainingSeconds ~/ secondsPerHour; + if (hours > 0 && parts.length < 2) { + parts.add('${hours}h'); + remainingSeconds %= secondsPerHour; + } + + // Calculate and add minutes + final int minutes = remainingSeconds ~/ secondsPerMinute; + if (minutes > 0 && parts.length < 2) { + parts.add('${minutes}m'); + remainingSeconds %= secondsPerMinute; } - return ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.onSurface), - controller: nodeIdController, - decoration: const InputDecoration( - labelText: 'Node ID', - )), - trailing: (widget.isV4) - ? null - : ElevatedButton( - onPressed: null, - style: ElevatedButton.styleFrom( - disabledBackgroundColor: statusColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)))), - child: Text( - widget.node.status.name, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: statusTextColor), - ), + // Add remaining seconds if necessary + final int seconds = remainingSeconds; + if ((seconds > 0 && parts.length < 2) || parts.isEmpty) { + if (remainingSeconds > 0 || parts.isEmpty) { + parts.add('${seconds}s'); + } + } + + if (parts.isEmpty) { + return '${totalSeconds}s'; + } + + return parts.join(' '); + } + + @override + Widget build(BuildContext context) { + String? countryCode = countryNameToCode[widget.node.country]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: countryCode != null + ? ClipOval( + child: SizedBox( + width: 37, + height: 37, + child: Flag.fromString(countryCode, fit: BoxFit.cover), + ), + ) + : ClipOval( + child: Container( + width: 37, + height: 37, + color: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + ), + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Text( + 'Node ID: ${widget.node.nodeId}', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8.0), + + // Country Chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text(widget.node.country!, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: Theme.of(context).colorScheme.primary, + )), + ), + const SizedBox(width: 8.0), + + // Status Chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all( + color: widget.node.status == NodeStatus.Up + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text(widget.node.status.name, + style: + Theme.of(context).textTheme.bodySmall!.copyWith( + color: widget.node.status == NodeStatus.Up + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + )), + ), + ], + ), + const SizedBox(height: 6.0), + Text( + 'Farm: ${widget.farmName}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4.0), + Text( + 'Uptime: ${_formatUptime(widget.node.uptime ?? 0)}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), + ), + ], + ), ); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 802b54eb5..fbb5c36fc 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -382,6 +382,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + enum_to_string: + dependency: transitive + description: + name: enum_to_string + sha256: "93b75963d3b0c9f6a90c095b3af153e1feccb79f6f08282d3274ff8d9eea52bc" + url: "https://pub.dev" + source: hosted + version: "2.2.1" equatable: dependency: transitive description: @@ -422,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flag: + dependency: "direct main" + description: + name: flag + sha256: cfca88049e769caf7ec439e6fbd23fd7ff4f332757796a0b7c09d94579c1ea70 + url: "https://pub.dev" + source: hosted + version: "7.0.0" flagsmith: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 0bda59bb5..6067ed405 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -84,6 +84,7 @@ dependencies: infinite_scroll_pagination: ^4.1.0 intl_mobile_field: ^1.1.1 mobile_scanner: 5.2.3 + flag: ^7.0.0 dev_dependencies: flutter_test: sdk: flutter From 27c85733c12ac7d9db870287d414726d4aa98a4e Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 30 Apr 2025 16:01:32 +0300 Subject: [PATCH 2/6] Fix padding & spacing, refactor farm node item, remove hardcode farm item colors --- app/lib/screens/farm_details.dart | 13 +++--- app/lib/widgets/farm_item.dart | 67 +++++++-------------------- app/lib/widgets/farm_node_item.dart | 70 ++++++++++++++--------------- 3 files changed, 55 insertions(+), 95 deletions(-) diff --git a/app/lib/screens/farm_details.dart b/app/lib/screens/farm_details.dart index 52ca623ac..403e34207 100644 --- a/app/lib/screens/farm_details.dart +++ b/app/lib/screens/farm_details.dart @@ -541,13 +541,10 @@ class _FarmDetailsState extends State { ), ), clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.only(top: 6.0, bottom: 6.0), // Increased top padding - child: FarmNodeItemWidget( - node: node, - isV4: widget.isV4, - farmName: widget.farm.name, - ), + child: FarmNodeItemWidget( + node: node, + isV4: widget.isV4, + farmName: widget.farm.name, ), ); }, @@ -562,4 +559,4 @@ class _FarmDetailsState extends State { ), ); } -} \ No newline at end of file +} diff --git a/app/lib/widgets/farm_item.dart b/app/lib/widgets/farm_item.dart index 4889f01a6..b10d424cc 100644 --- a/app/lib/widgets/farm_item.dart +++ b/app/lib/widgets/farm_item.dart @@ -20,47 +20,15 @@ class FarmItemWidget extends StatefulWidget { } class _FarmItemWidgetState extends State { - final walletAddressController = TextEditingController(); - final tfchainWalletSecretController = TextEditingController(); - final walletNameController = TextEditingController(); - final twinIdController = TextEditingController(); - final farmIdController = TextEditingController(); - - bool showTfchainSecret = false; - bool edit = false; - bool isSaving = false; - - final walletFocus = FocusNode(); - ChainType chainType = ChainType.Stellar; - - String? addressError; - String? currentAddress; - - @override - void initState() { - super.initState(); - currentAddress = widget.farm.walletAddress; - walletAddressController.text = currentAddress!; - - tfchainWalletSecretController.text = widget.farm.tfchainWalletSecret; - walletNameController.text = widget.farm.walletName; - farmIdController.text = widget.farm.farmId.toString(); - twinIdController.text = widget.farm.twinId.toString(); - } - @override void dispose() { - walletAddressController.dispose(); - tfchainWalletSecretController.dispose(); - walletNameController.dispose(); - twinIdController.dispose(); - farmIdController.dispose(); - walletFocus.dispose(); super.dispose(); } - + @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( onTap: () { Navigator.push( @@ -81,17 +49,17 @@ class _FarmItemWidgetState extends State { borderRadius: BorderRadius.circular(8.0), ), clipBehavior: Clip.antiAlias, - color: Colors.grey[850], + color: colorScheme.surfaceVariant, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Padding( - padding: EdgeInsets.only(right: 16.0), + Padding( + padding: const EdgeInsets.only(right: 16.0), child: Icon( Icons.menu, - color: Colors.white70, + color: colorScheme.onSurfaceVariant.withOpacity(0.7), ), ), Expanded( @@ -101,29 +69,24 @@ class _FarmItemWidgetState extends State { children: [ Text( widget.farm.name, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - color: Colors.white, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.farm.nodes.isNotEmpty) + ...[ const SizedBox(height: 4.0), - if (widget.farm.nodes.isNotEmpty) Text( '${widget.farm.nodes.length} Node${widget.farm.nodes.length == 1 ? '' : 's'}', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: Colors.white70, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: + colorScheme.onSurfaceVariant.withOpacity(0.7), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), + ], ], ), ), @@ -133,4 +96,4 @@ class _FarmItemWidgetState extends State { ), ); } -} \ No newline at end of file +} diff --git a/app/lib/widgets/farm_node_item.dart b/app/lib/widgets/farm_node_item.dart index 31f508ecd..8f018dbce 100644 --- a/app/lib/widgets/farm_node_item.dart +++ b/app/lib/widgets/farm_node_item.dart @@ -73,16 +73,42 @@ class _FarmNodeItemWidgetState extends State { return parts.join(' '); } + Widget _buildChip({ + required String label, + required Color borderColor, + required Color textColor, + }) { + return Container( + padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: textColor, + ), + ), + ); + } + @override Widget build(BuildContext context) { String? countryCode = countryNameToCode[widget.node.country]; + final statusColor = widget.node.status == NodeStatus.Up + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error; + return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 6.0), child: countryCode != null ? ClipOval( child: SizedBox( @@ -122,44 +148,18 @@ class _FarmNodeItemWidgetState extends State { const SizedBox(width: 8.0), // Country Chip - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(20), - ), - child: Text(widget.node.country!, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: Theme.of(context).colorScheme.primary, - )), + _buildChip( + label: widget.node.country!, + borderColor: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8.0), // Status Chip - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border.all( - color: widget.node.status == NodeStatus.Up - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.error, - ), - borderRadius: BorderRadius.circular(20), - ), - child: Text(widget.node.status.name, - style: - Theme.of(context).textTheme.bodySmall!.copyWith( - color: widget.node.status == NodeStatus.Up - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.error, - )), + _buildChip( + label: widget.node.status.name, + borderColor: statusColor, + textColor: statusColor, ), ], ), From 551ead2e18258f848cdd4aa58443fdc05389d7a7 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 30 Apr 2025 18:53:15 +0300 Subject: [PATCH 3/6] Remove tfchain divider if no nodes --- app/lib/screens/farm_details.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/screens/farm_details.dart b/app/lib/screens/farm_details.dart index 403e34207..2671c45fd 100644 --- a/app/lib/screens/farm_details.dart +++ b/app/lib/screens/farm_details.dart @@ -504,12 +504,12 @@ class _FarmDetailsState extends State { Theme.of(context).colorScheme.onSurfaceVariant, ), ), - const Divider(height: 20, thickness: 1), ], if (widget.farm.nodes.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Divider(height: 20, thickness: 1), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( From 199e8ad4d037b09e0e9a5963d82cc7a96c31926d Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Thu, 1 May 2025 10:22:41 +0300 Subject: [PATCH 4/6] Fix country in node class --- app/lib/models/farm.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/models/farm.dart b/app/lib/models/farm.dart index 8310e67f4..e52041f76 100644 --- a/app/lib/models/farm.dart +++ b/app/lib/models/farm.dart @@ -4,7 +4,7 @@ class Node { Node({ required this.nodeId, required this.status, - required this.country, + this.country, this.uptime, this.updatedAt, }); From 6501cf3264819bea9da097fd02ab6a82a36e5566 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Thu, 1 May 2025 10:23:36 +0300 Subject: [PATCH 5/6] Fix warnings --- app/lib/widgets/wizard/common_page.dart | 4 ++-- app/lib/widgets/wizard/terms_and_conditions.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/widgets/wizard/common_page.dart b/app/lib/widgets/wizard/common_page.dart index 46e7dd425..5e7af5cb1 100644 --- a/app/lib/widgets/wizard/common_page.dart +++ b/app/lib/widgets/wizard/common_page.dart @@ -15,7 +15,7 @@ class CommonPage extends StatefulWidget { final bool? showTermsAndConditions; const CommonPage({ - Key? key, + super.key, required this.title, required this.subtitle, required this.imagePath, @@ -23,7 +23,7 @@ class CommonPage extends StatefulWidget { this.heightPercentage = 100, this.widthPercentage = 300, this.showTermsAndConditions = false, - }) : super(key: key); + }); @override State createState() => _CommonPageState(); diff --git a/app/lib/widgets/wizard/terms_and_conditions.dart b/app/lib/widgets/wizard/terms_and_conditions.dart index 48cb22a8a..db0ea99b5 100644 --- a/app/lib/widgets/wizard/terms_and_conditions.dart +++ b/app/lib/widgets/wizard/terms_and_conditions.dart @@ -5,7 +5,7 @@ import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/helpers/globals.dart'; class TermsAndConditions extends StatefulWidget { - const TermsAndConditions({Key? key}) : super(key: key); + const TermsAndConditions({super.key}); @override State createState() => _TermsAndConditionsState(); From ed82c35b5a4eb3de2a41e32b6d24139663ebf5e7 Mon Sep 17 00:00:00 2001 From: zaelgohary Date: Wed, 7 May 2025 12:07:06 +0300 Subject: [PATCH 6/6] Change farm icon, remove nav.pop after selecting address --- app/lib/screens/farm_details.dart | 1 - app/lib/widgets/farm_item.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/screens/farm_details.dart b/app/lib/screens/farm_details.dart index 2671c45fd..603bb1708 100644 --- a/app/lib/screens/farm_details.dart +++ b/app/lib/screens/farm_details.dart @@ -202,7 +202,6 @@ class _FarmDetailsState extends State { walletAddressController.text = address; _validateStellarAddress(address.trim()); }); - Navigator.pop(context); } Widget _buildDisplayField({ diff --git a/app/lib/widgets/farm_item.dart b/app/lib/widgets/farm_item.dart index b10d424cc..01c48c9d6 100644 --- a/app/lib/widgets/farm_item.dart +++ b/app/lib/widgets/farm_item.dart @@ -58,7 +58,7 @@ class _FarmItemWidgetState extends State { Padding( padding: const EdgeInsets.only(right: 16.0), child: Icon( - Icons.menu, + Icons.storage, color: colorScheme.onSurfaceVariant.withOpacity(0.7), ), ),