From e0a8f32d69e04c5a6f66670f1f9f1ac0358e746d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 2 Nov 2022 15:03:14 -0600 Subject: [PATCH] desktop wallet network settings expanding node cards --- .../sub_widgets/network_info_button.dart | 2 +- .../custom_buttons/blue_text_button.dart | 65 ++-- lib/widgets/node_card.dart | 360 ++++++++++++++---- 3 files changed, 329 insertions(+), 98 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 37cd414c7..5dc8c4723 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -157,7 +157,7 @@ class _NetworkInfoButtonState extends ConsumerState { showDialog( context: context, builder: (context) => DesktopDialog( - maxHeight: 600, + maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index 18757ab93..aa7f75b1f 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -5,11 +5,16 @@ import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; class BlueTextButton extends ConsumerStatefulWidget { - const BlueTextButton({Key? key, required this.text, this.onTap}) - : super(key: key); + const BlueTextButton({ + Key? key, + required this.text, + this.onTap, + this.enabled = true, + }) : super(key: key); final String text; final VoidCallback? onTap; + final bool enabled; @override ConsumerState createState() => _BlueTextButtonState(); @@ -17,38 +22,42 @@ class BlueTextButton extends ConsumerStatefulWidget { class _BlueTextButtonState extends ConsumerState with SingleTickerProviderStateMixin { - late AnimationController controller; - late Animation animation; + AnimationController? controller; + Animation? animation; late Color color; @override void initState() { - color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; - controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 100), - ); - animation = ColorTween( - begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, - end: ref - .read(colorThemeProvider.state) - .state - .buttonTextBorderless - .withOpacity(0.4), - ).animate(controller); + if (widget.enabled) { + color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + animation = ColorTween( + begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, + end: ref + .read(colorThemeProvider.state) + .state + .buttonTextBorderless + .withOpacity(0.4), + ).animate(controller!); - animation.addListener(() { - setState(() { - color = animation.value as Color; + animation!.addListener(() { + setState(() { + color = animation!.value as Color; + }); }); - }); + } else { + color = ref.read(colorThemeProvider.state).state.textSubtitle1; + } super.initState(); } @override void dispose() { - controller.dispose(); + controller?.dispose(); super.dispose(); } @@ -59,11 +68,13 @@ class _BlueTextButtonState extends ConsumerState text: TextSpan( text: widget.text, style: STextStyles.link2(context).copyWith(color: color), - recognizer: TapGestureRecognizer() - ..onTap = () { - widget.onTap?.call(); - controller.forward().then((value) => controller.reverse()); - }, + recognizer: widget.enabled + ? (TapGestureRecognizer() + ..onTap = () { + widget.onTap?.call(); + controller?.forward().then((value) => controller?.reverse()); + }) + : null, ), ); } diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 1f0287013..bf9d2746e 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -1,15 +1,31 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/test_epic_box_connection.dart'; +import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/node_options_sheet.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class NodeCard extends ConsumerStatefulWidget { const NodeCard({ @@ -30,6 +46,125 @@ class NodeCard extends ConsumerStatefulWidget { class _NodeCardState extends ConsumerState { String _status = "Disconnected"; late final String nodeId; + bool _advancedIsExpanded = true; + + Future _notifyWalletsOfUpdatedNode(WidgetRef ref) async { + final managers = ref + .read(walletsChangeNotifierProvider) + .managers + .where((e) => e.coin == widget.coin); + final prefs = ref.read(prefsChangeNotifierProvider); + + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + for (final manager in managers) { + if (manager.isActiveWallet) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.selectedWalletsAtStartup: + final List walletIdsToSync = prefs.walletIdsSyncOnStartup; + for (final manager in managers) { + if (walletIdsToSync.contains(manager.walletId)) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.allWalletsOnStartup: + for (final manager in managers) { + manager.updateNode(true); + } + break; + } + } + + Future _testConnection( + NodeModel node, + BuildContext context, + WidgetRef ref, + ) async { + bool testPassed = false; + + switch (widget.coin) { + case Coin.epicCash: + try { + final String uriString = "${node.host}:${node.port}/v1/version"; + + testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + break; + + case Coin.monero: + case Coin.wownero: + try { + final uri = Uri.parse(node.host); + if (uri.scheme.startsWith("http")) { + final String path = uri.path.isEmpty ? "/json_rpc" : uri.path; + + String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; + + testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + + break; + + case Coin.bitcoin: + case Coin.litecoin: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.firoTestNet: + case Coin.dogecoinTestNet: + case Coin.bitcoincash: + case Coin.litecoinTestNet: + case Coin.namecoin: + case Coin.bitcoincashTestnet: + final client = ElectrumX( + host: node.host, + port: node.port, + useSSL: node.useSSL, + failovers: [], + prefs: ref.read(prefsChangeNotifierProvider), + ); + + try { + testPassed = await client.ping(); + } catch (_) { + testPassed = false; + } + + break; + } + + if (testPassed) { + // showFloatingFlushBar( + // type: FlushBarType.success, + // message: "Server ping success", + // context: context, + // ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + iconAsset: Assets.svg.circleAlert, + message: "Could not connect to node", + context: context, + ), + ); + } + + return testPassed; + } @override void initState() { @@ -50,91 +185,176 @@ class _NodeCardState extends ConsumerState { _status = "Disconnected"; } + final isDesktop = Util.isDesktop; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => NodeOptionsSheet( - nodeId: nodeId, - coin: widget.coin, - popBackToRoute: widget.popBackToRoute, + borderColor: isDesktop + ? Theme.of(context).extension()!.background + : null, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => NodeOptionsSheet( + nodeId: nodeId, + coin: widget.coin, + popBackToRoute: widget.popBackToRoute, + ), + ); + }, + child: child, ); }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: _node.name == DefaultNodes.defaultName - ? Theme.of(context) - .extension()! - .buttonBackSecondary - : Theme.of(context) - .extension()! - .infoItemIcons - .withOpacity(0.2), - borderRadius: BorderRadius.circular(100), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: child, + body: Padding( + padding: const EdgeInsets.only( + bottom: 24, ), - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - height: 11, - width: 14, + child: Row( + children: [ + const SizedBox( + width: 66, + ), + BlueTextButton( + text: "Connect", + enabled: _status == "Disconnected", + onTap: () async { + final canConnect = + await _testConnection(_node, context, ref); + if (!canConnect) { + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor( + coin: widget.coin, + node: _node, + shouldNotifyListeners: true, + ); + + await _notifyWalletsOfUpdatedNode(ref); + }, + ), + const SizedBox( + width: 48, + ), + BlueTextButton( + text: "Details", + onTap: () { + Navigator.of(context).pushNamed( + NodeDetailsView.routeName, + arguments: Tuple3( + widget.coin, + widget.nodeId, + widget.popBackToRoute, + ), + ); + }, + ), + ], + ), + ), + ); + }, + child: Padding( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + child: Row( + children: [ + Container( + width: isDesktop ? 40 : 24, + height: isDesktop ? 40 : 24, + decoration: BoxDecoration( color: _node.name == DefaultNodes.defaultName ? Theme.of(context) .extension()! - .accentColorDark + .buttonBackSecondary : Theme.of(context) .extension()! - .infoItemIcons, + .infoItemIcons + .withOpacity(0.2), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + height: isDesktop ? 18 : 11, + width: isDesktop ? 20 : 14, + color: _node.name == DefaultNodes.defaultName + ? Theme.of(context) + .extension()! + .accentColorDark + : Theme.of(context) + .extension()! + .infoItemIcons, + ), ), ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _node.name, - style: STextStyles.titleBold12(context), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _node.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + Text( + _status, + style: STextStyles.label(context), + ), + ], + ), + const Spacer(), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.network, + color: _status == "Connected" + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .buttonBackSecondary, + width: 20, + height: 20, ), - const SizedBox( - height: 2, - ), - Text( - _status, - style: STextStyles.label(context), - ), - ], - ), - const Spacer(), - SvgPicture.asset( - Assets.svg.network, - color: _status == "Connected" - ? Theme.of(context) + if (isDesktop) + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 12, + height: 6, + color: Theme.of(context) .extension()! - .accentColorGreen - : Theme.of(context) - .extension()! - .buttonBackSecondary, - width: 20, - height: 20, - ), - ], + .textSubtitle1, + ), + ], + ), ), ), ),