desktop wallet network settings expanding node cards

This commit is contained in:
julian 2022-11-02 15:03:14 -06:00
parent 2afec92279
commit e0a8f32d69
3 changed files with 329 additions and 98 deletions

View file

@ -157,7 +157,7 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (context) => DesktopDialog( builder: (context) => DesktopDialog(
maxHeight: 600, maxHeight: MediaQuery.of(context).size.height - 64,
maxWidth: 580, maxWidth: 580,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View file

@ -5,11 +5,16 @@ import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
class BlueTextButton extends ConsumerStatefulWidget { class BlueTextButton extends ConsumerStatefulWidget {
const BlueTextButton({Key? key, required this.text, this.onTap}) const BlueTextButton({
: super(key: key); Key? key,
required this.text,
this.onTap,
this.enabled = true,
}) : super(key: key);
final String text; final String text;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool enabled;
@override @override
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState(); ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
@ -17,38 +22,42 @@ class BlueTextButton extends ConsumerStatefulWidget {
class _BlueTextButtonState extends ConsumerState<BlueTextButton> class _BlueTextButtonState extends ConsumerState<BlueTextButton>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController controller; AnimationController? controller;
late Animation<dynamic> animation; Animation<dynamic>? animation;
late Color color; late Color color;
@override @override
void initState() { void initState() {
color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; if (widget.enabled) {
controller = AnimationController( color = ref.read(colorThemeProvider.state).state.buttonTextBorderless;
vsync: this, controller = AnimationController(
duration: const Duration(milliseconds: 100), vsync: this,
); duration: const Duration(milliseconds: 100),
animation = ColorTween( );
begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, animation = ColorTween(
end: ref begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless,
.read(colorThemeProvider.state) end: ref
.state .read(colorThemeProvider.state)
.buttonTextBorderless .state
.withOpacity(0.4), .buttonTextBorderless
).animate(controller); .withOpacity(0.4),
).animate(controller!);
animation.addListener(() { animation!.addListener(() {
setState(() { setState(() {
color = animation.value as Color; color = animation!.value as Color;
});
}); });
}); } else {
color = ref.read(colorThemeProvider.state).state.textSubtitle1;
}
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
controller.dispose(); controller?.dispose();
super.dispose(); super.dispose();
} }
@ -59,11 +68,13 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
text: TextSpan( text: TextSpan(
text: widget.text, text: widget.text,
style: STextStyles.link2(context).copyWith(color: color), style: STextStyles.link2(context).copyWith(color: color),
recognizer: TapGestureRecognizer() recognizer: widget.enabled
..onTap = () { ? (TapGestureRecognizer()
widget.onTap?.call(); ..onTap = () {
controller.forward().then((value) => controller.reverse()); widget.onTap?.call();
}, controller?.forward().then((value) => controller?.reverse());
})
: null,
), ),
); );
} }

View file

@ -1,15 +1,31 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.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/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.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/node_options_sheet.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class NodeCard extends ConsumerStatefulWidget { class NodeCard extends ConsumerStatefulWidget {
const NodeCard({ const NodeCard({
@ -30,6 +46,125 @@ class NodeCard extends ConsumerStatefulWidget {
class _NodeCardState extends ConsumerState<NodeCard> { class _NodeCardState extends ConsumerState<NodeCard> {
String _status = "Disconnected"; String _status = "Disconnected";
late final String nodeId; late final String nodeId;
bool _advancedIsExpanded = true;
Future<void> _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<String> 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<bool> _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 @override
void initState() { void initState() {
@ -50,91 +185,176 @@ class _NodeCardState extends ConsumerState<NodeCard> {
_status = "Disconnected"; _status = "Disconnected";
} }
final isDesktop = Util.isDesktop;
return RoundedWhiteContainer( return RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RawMaterialButton( borderColor: isDesktop
shape: RoundedRectangleBorder( ? Theme.of(context).extension<StackColors>()!.background
borderRadius: BorderRadius.circular( : null,
Constants.size.circularBorderRadius, child: ConditionalParent(
), condition: !isDesktop,
), builder: (child) {
onPressed: () { return RawMaterialButton(
showModalBottomSheet<dynamic>( shape: RoundedRectangleBorder(
backgroundColor: Colors.transparent, borderRadius: BorderRadius.circular(
context: context, Constants.size.circularBorderRadius,
builder: (_) => NodeOptionsSheet( ),
nodeId: nodeId,
coin: widget.coin,
popBackToRoute: widget.popBackToRoute,
), ),
onPressed: () {
showModalBottomSheet<void>(
backgroundColor: Colors.transparent,
context: context,
builder: (_) => NodeOptionsSheet(
nodeId: nodeId,
coin: widget.coin,
popBackToRoute: widget.popBackToRoute,
),
);
},
child: child,
); );
}, },
child: Padding( child: ConditionalParent(
padding: const EdgeInsets.all(12), condition: isDesktop,
child: Row( builder: (child) {
children: [ return Expandable(
Container( onExpandChanged: (state) {
width: 24, setState(() {
height: 24, _advancedIsExpanded = state == ExpandableState.expanded;
decoration: BoxDecoration( });
color: _node.name == DefaultNodes.defaultName },
? Theme.of(context) header: child,
.extension<StackColors>()! body: Padding(
.buttonBackSecondary padding: const EdgeInsets.only(
: Theme.of(context) bottom: 24,
.extension<StackColors>()!
.infoItemIcons
.withOpacity(0.2),
borderRadius: BorderRadius.circular(100),
), ),
child: Center( child: Row(
child: SvgPicture.asset( children: [
Assets.svg.node, const SizedBox(
height: 11, width: 66,
width: 14, ),
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 color: _node.name == DefaultNodes.defaultName
? Theme.of(context) ? Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.accentColorDark .buttonBackSecondary
: Theme.of(context) : Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.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<StackColors>()!
.accentColorDark
: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
), ),
), ),
), const SizedBox(
const SizedBox( width: 12,
width: 12, ),
), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( _node.name,
_node.name, style: STextStyles.titleBold12(context),
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<StackColors>()!
.accentColorGreen
: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
width: 20,
height: 20,
), ),
const SizedBox( if (isDesktop)
height: 2, SvgPicture.asset(
), _advancedIsExpanded
Text( ? Assets.svg.chevronDown
_status, : Assets.svg.chevronUp,
style: STextStyles.label(context), width: 12,
), height: 6,
], color: Theme.of(context)
),
const Spacer(),
SvgPicture.asset(
Assets.svg.network,
color: _status == "Connected"
? Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.accentColorGreen .textSubtitle1,
: Theme.of(context) ),
.extension<StackColors>()! ],
.buttonBackSecondary, ),
width: 20,
height: 20,
),
],
), ),
), ),
), ),