new-node-list-ui-on-dev (#3259)

* node list new ui # Conflicts: # cw_core/lib/node_list.dart * increase touch target sizes * fix scroll behavior * fix concurrent modification * Update lib/src/screens/settings/manage_nodes_page.dart [skip ci] * Update lib/src/screens/nodes/node_create_or_edit_page.dart [skip ci] * run speed test in parallel not sequential --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

malik1004x committed Jun 1, 2026 at 21:32 UTC 6c1ee1651349cf02ee05c1edc26137118f59bbf9
20 files changed +472 -404
cw_core/lib/node.dart
+23 -3
@@ -132,6 +132,26 @@ class Node {
132 };
133 }
134
135 + Node.fromUri(Uri uri, WalletType type)
136 + : id = 0,
137 + uriRaw =
138 + "${uri.host}${uri.hasPort ? ':${uri.port}' : (uri.queryParameters['port']?.isNotEmpty == true ? ':${uri.queryParameters['port']}' : '')}",
139 + path = uri.path,
140 + login = uri.userInfo.contains(':')
141 + ? uri.userInfo.split(':')[0]
142 + : uri.queryParameters['username'],
143 + password = uri.userInfo.contains(':')
144 + ? uri.userInfo.split(':')[1]
145 + : uri.queryParameters['password'],
146 + useSSL = uri.queryParameters['protocol'] == 'https',
147 + trusted = uri.queryParameters['trusted'] == 'true',
148 + isPow = false,
149 + isEnabledForAutoSwitching = false,
150 + isOfficial = false,
151 + isBuiltin = false,
152 + isDefault = false,
153 + typeRaw = serializeToInt(type);
154 +
155 Future<int> delete() async {
156 return await db!.delete(tableName, where: '${selfIdColumn} = ?', whereArgs: [id]);
157 }
@@ -378,14 +398,14 @@ class Node {
398 final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
399
400 try {
381 - final client = ProxyWrapper().getHttpIOClient();
401
402 final jsonBody = json.encode(body);
403
385 - final response = await client.post(
386 - rpcUri,
404 + final response = await ProxyWrapper().post(
405 + clearnetUri: rpcUri,
406 headers: {'Content-Type': 'application/json'},
407 body: jsonBody,
408 + allowMitmMoneroBypassSSLCheck: true
409 );
410 // Check if we received a 401 Unauthorized response
411 if (response.statusCode == 401) {
cw_core/lib/node_list.dart
+6 -2
@@ -1,5 +1,4 @@
1 import 'package:flutter/services.dart';
2 -import 'package:hive/hive.dart';
2 import "package:yaml/yaml.dart";
3 import 'package:cw_core/node.dart';
4 import 'package:cw_core/wallet_type.dart';
@@ -78,6 +77,7 @@ Future<List<Node>> loadDefaultNodes(WalletType type) async {
77 map["isBuiltin"] = 1;
78 map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
79 map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
80 +
81 final node = Node.fromMap(Map<String, Object>.from(map));
82 node.type = type;
83 nodes.add(node);
@@ -94,7 +94,11 @@ Future<List<Node>> loadDefaultNanoPowNodes() async {
94
95 for (final raw in loadedPowNodes) {
96 if (raw is Map) {
97 - final node = Node.fromMap(Map<String, Object>.from(raw));
97 + final map = Map.from(raw);
98 + map["isBuiltin"] = 1;
99 + map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
100 + map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
101 + final node = Node.fromMap(Map<String, Object>.from(map));
102 node.isPow = true;
103 node.type = WalletType.nano;
104 nodes.add(node);
lib/di.dart
+3 -13
@@ -1118,9 +1118,9 @@ Future<void> setup({
1118
1119 getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
1120
1121 - getIt.registerLazySingleton(() {
1121 + getIt.registerFactoryParam<NodeListViewModel, bool, void>((isPow, _) {
1122 final appStore = getIt.get<AppStore>();
1123 - return NodeListViewModel(appStore);
1123 + return NodeListViewModel(appStore, isPow);
1124 });
1125
1126 getIt.registerFactoryParam<NewAddressesPage, bool, void>(
@@ -1131,11 +1131,6 @@ Future<void> setup({
1131 ),
1132 );
1133
1134 - getIt.registerLazySingleton(() {
1135 - final appStore = getIt.get<AppStore>();
1136 - return PowNodeListViewModel(appStore);
1137 - });
1138 -
1134 getIt.registerFactory(() => ConnectionSyncViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
1135
1136 getIt.registerFactory(() => ConnectionSyncPage(getIt.get<ConnectionSyncViewModel>()));
@@ -1178,8 +1173,6 @@ Future<void> setup({
1173 final Node? editingNode = args['editingNode'] as Node?;
1174 return NodeCreateOrEditViewModel(
1175 isPow,
1181 - getIt.get<NodeListViewModel>(),
1182 - getIt.get<PowNodeListViewModel>(),
1176 type,
1177 getIt.get<SettingsStore>(),
1178 editingNode: editingNode
@@ -1700,10 +1693,7 @@ Future<void> setup({
1693 );
1694
1695 getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
1703 - if (isPow) {
1704 - return ManageNodesPage(isPow, powNodeListViewModel: getIt.get<PowNodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
1705 - }
1706 - return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
1696 + return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(param1: isPow), dashboardViewModel: getIt.get<DashboardViewModel>());
1697 });
1698
1699 getIt.registerFactory(
lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
+7 -2
@@ -17,10 +17,10 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart';
17 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
18 import 'package:cw_core/crypto_currency.dart';
19 import 'package:cw_core/lnurl.dart';
20 +import 'package:cw_core/node.dart';
21 import 'package:cw_core/unspent_coin_type.dart';
22 import 'package:cw_core/wallet_type.dart';
23 import 'package:flutter/material.dart';
23 -import 'package:flutter_svg/svg.dart';
24 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
25
26 import '../../../pages/receive_page.dart';
@@ -198,7 +198,12 @@ class CoinActionRow extends StatelessWidget {
198 Navigator.of(context)
199 .pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
200 return;
201 - } else {
201 + } else if(["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
202 + Navigator.of(context).pushNamed(Routes.newNode,
203 + arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
204 + return;
205 +
206 + }else {
207 final uri = Uri.tryParse(code);
208 if (uri == null) return;
209 req = PaymentRequest.fromUri(uri);
lib/new-ui/widgets/modal_header.dart
+7 -2
@@ -4,11 +4,12 @@ import 'package:flutter_svg/svg.dart';
4
5 class ModalHeader extends StatelessWidget {
6 const ModalHeader(
7 - {super.key, required this.iconPath, required this.message, required this.title});
7 + {super.key, required this.iconPath, required this.message, required this.title, this.bottomWidget});
8
9 final String iconPath;
10 final String title;
11 final String message;
12 + final Widget? bottomWidget;
13
14 @override
15 Widget build(BuildContext context) {
@@ -41,7 +42,11 @@ class ModalHeader extends StatelessWidget {
42 fontWeight: FontWeight.w400,
43 color: Theme.of(context).colorScheme.onSurfaceVariant),
44 textAlign: TextAlign.center,
44 - ))
45 + )),
46 + if(bottomWidget != null) ...[
47 + Padding(padding: EdgeInsets.symmetric(horizontal: 18), child: Container(height:1, color: Theme.of(context).colorScheme.surfaceContainerHigh),),
48 + bottomWidget!
49 + ]
50 ],
51 ),
52 ),
lib/src/screens/nodes/node_create_or_edit_page.dart
+61 -77
@@ -1,20 +1,20 @@
1 import 'package:cake_wallet/core/execution_state.dart';
2 +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
5 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6 +import 'package:cake_wallet/src/screens/nodes/node_share_page.dart';
7 import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
8 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
9 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
8 -import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
9 -import 'package:cake_wallet/src/widgets/primary_button.dart';
10 -import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
10 +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
11 import 'package:cake_wallet/utils/show_pop_up.dart';
12 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
13 +import 'package:cw_core/currency_for_wallet_type.dart';
14 import 'package:cw_core/node.dart';
15 import 'package:cw_core/wallet_type.dart';
16 +import 'package:flutter/cupertino.dart';
17 import 'package:flutter/material.dart';
16 -import 'package:flutter_mobx/flutter_mobx.dart';
17 -import 'package:flutter_svg/svg.dart';
18 import 'package:mobx/mobx.dart';
19
20 class NodeCreateOrEditPage extends StatefulWidget {
@@ -76,6 +76,14 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
76 },
77 );
78
79 +
80 + reaction((_)=>widget.nodeCreateOrEditViewModel.state, (state) async {
81 + if(state is ExecutedSuccessfullyState) {
82 + await Future.delayed(Duration(milliseconds: 100));
83 + if (mounted) Navigator.of(context).pop(widget.nodeCreateOrEditViewModel.editingNode);
84 + }
85 +
86 + });
87 }
88
89 @override
@@ -90,86 +98,62 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
98 title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
99 leadingIcon: Icon(Icons.arrow_back_ios_new),
100 onLeadingPressed: Navigator.of(context).pop,
93 - trailingIcon: CakeImageWidget(imageUrl:
94 - "assets/new-ui/scan.svg",width:24,height:24,
95 - colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
96 - ),
97 - onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
101 + trailingIcon: Icon(Icons.check),
102 + onTrailingPressed: () async {
103 + if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
104 + return;
105 + }
106 +
107 + await widget.nodeCreateOrEditViewModel
108 + .save(saveAsCurrent: widget.isSelected ?? false);
109 + },
110 ),
111 Expanded(
112 child: KeyboardHideOverlay(
113 child: Container(
114 padding: const EdgeInsets.only(left: 18, right: 18),
103 - child: ScrollableWithBottomSection(
104 - contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
105 - content: NodeForm(
115 + child: Column(
116 + spacing: 18,
117 + children: [NodeForm(
118 key: _nodeFormKey,
119 nodeViewModel: widget.nodeCreateOrEditViewModel,
120 ),
109 - bottomSectionPadding: const EdgeInsets.only(bottom: 24),
110 - bottomSection: Observer(
111 - builder: (_) => Row(
112 - mainAxisAlignment: MainAxisAlignment.center,
113 - children: <Widget>[
114 - Flexible(
115 - child: Container(
116 - padding: const EdgeInsets.only(right: 8.0),
117 - child: LoadingPrimaryButton(
118 - onPressed: () async {
119 - final confirmed = await showPopUp<bool>(
120 - context: context,
121 - builder: (context) => AlertWithTwoActions(
122 - alertTitle: S.of(context).remove_node,
123 - alertContent: S.of(context).remove_node_message,
124 - rightButtonText: S.of(context).remove,
125 - leftButtonText: S.of(context).cancel,
126 - actionRightButton: () => Navigator.pop(context, true),
127 - actionLeftButton: () => Navigator.pop(context, false),
128 - ),
129 - ) ??
130 - false;
131 -
132 - if (confirmed) {
133 - await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
134 - Navigator.of(context).pop();
135 - }
136 - },
137 - text: S.of(context).delete,
138 - isDisabled: widget.editingNode == null ||
139 - !widget.nodeCreateOrEditViewModel.isReady ||
140 - widget.editingNode!.isBuiltin ||
141 - (widget.isSelected ?? false),
142 - color: Theme.of(context).colorScheme.errorContainer,
143 - textColor: Theme.of(context).colorScheme.onErrorContainer,
144 - ),
145 - ),
146 - ),
147 - Flexible(
148 - child: Container(
149 - padding: const EdgeInsets.only(left: 8.0),
150 - child: PrimaryButton(
151 - onPressed: () async {
152 - if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
153 - return;
154 - }
155 -
156 - await widget.nodeCreateOrEditViewModel.save(
157 - saveAsCurrent: widget.isSelected ?? false);
158 - if (context.mounted) {
159 - Navigator.of(context).pop();
160 - }
161 - },
162 - text: S.of(context).save,
163 - color: Theme.of(context).colorScheme.primary,
164 - textColor: Theme.of(context).colorScheme.onPrimary,
165 - isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) ||
166 - (widget.nodeCreateOrEditViewModel.connectionState is IsExecutingState),
167 - ),
168 - ),
169 - ),
170 - ],
171 - ),
172 - ),
121 + NewListSections(sections: {"":[
122 + if(widget.editingNode != null)
123 +
124 + ListItemRegularRow(keyValue: "share", label: S.of(context).share_this_node, foregroundColor: Theme.of(context).colorScheme.primary, showArrow: false, onTap: (){
125 +
126 + Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>NodeSharePage(uri: widget.editingNode!.uri, currency: walletTypeToCryptoCurrency(widget.type!))));
127 +
128 +
129 + }),
130 + if(!(widget.editingNode == null ||
131 + !widget.nodeCreateOrEditViewModel.isReady ||
132 + widget.editingNode!.isBuiltin ||
133 + (widget.isSelected ?? false)))
134 + ListItemRegularRow(keyValue: "delete", label: S.of(context).delete_this_node, foregroundColor: Theme.of(context).colorScheme.errorContainer, showArrow: false, onTap: ()async{
135 + final confirmed = await showPopUp<bool>(
136 + context: context,
137 + builder: (context) => AlertWithTwoActions(
138 + alertTitle: S.of(context).remove_node,
139 + alertContent: S.of(context).remove_node_message,
140 + rightButtonText: S.of(context).remove,
141 + leftButtonText: S.of(context).cancel,
142 + actionRightButton: () => Navigator.pop(context, true),
143 + actionLeftButton: () => Navigator.pop(context, false),
144 + ),
145 + ) ??
146 + false;
147 +
148 + if (confirmed) {
149 + await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
150 + Navigator.of(context).pop();
151 + }
152 +
153 + })
154 + ]})
155 +
156 + ]
157 ),
158 ),
159 ),
lib/src/screens/nodes/node_share_page.dart new
+82
@@ -0,0 +1,82 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
3 +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
4 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 +import 'package:flutter/material.dart';
7 +
8 +class NodeSharePage extends StatelessWidget {
9 + const NodeSharePage({super.key, required this.uri, required this.currency});
10 +
11 + final Uri uri;
12 + final CryptoCurrency currency;
13 +
14 + @override
15 + Widget build(BuildContext context) {
16 + final qrSize = MediaQuery.of(context).size.width * 0.786;
17 + final padding = 16.0;
18 + return Container(
19 + color: Theme.of(context).colorScheme.surface,
20 + child: SafeArea(
21 + child: Column(
22 + mainAxisSize: MainAxisSize.max,
23 + children: [
24 + ModalTopBar(
25 + title: S.of(context).share,
26 + leadingIcon: Icon(Icons.arrow_back_ios_new),
27 + onLeadingPressed: Navigator.of(context).pop,
28 + ),
29 + Expanded(
30 + child: Column(
31 + mainAxisAlignment: MainAxisAlignment.center,
32 + spacing: 40,
33 + children: [
34 + Stack(alignment: Alignment.center, children: [
35 + SizedBox(
36 + width: qrSize,
37 + height: qrSize,
38 + child: ClipRRect(
39 + borderRadius: BorderRadius.circular(18),
40 + child: QrImage(
41 + size: qrSize - padding * 2,
42 + embeddedImagePath: "",
43 + data: uri.toString())),
44 + ),
45 + SizedBox(
46 + width: 60,
47 + height: 60,
48 + child: Stack(
49 + children: [
50 + CakeImageWidget(
51 + imageUrl: "assets/new-ui/settings_row_icons/nodes.svg",
52 + width: 54,
53 + height: 54,
54 + ),
55 + Align(
56 + alignment: Alignment.bottomRight,
57 + child: CakeImageWidget(
58 + imageUrl: currency.iconPath,
59 + width: 24,
60 + height: 24,
61 + ))
62 + ],
63 + ),
64 + ),
65 + ]),
66 + Padding(
67 + padding: const EdgeInsets.symmetric(horizontal: 36.0),
68 + child: Text(S.of(context).node_share_desc,
69 + textAlign: TextAlign.center,
70 + style: TextStyle(
71 + fontWeight: FontWeight.w500,
72 + color: Theme.of(context).colorScheme.onSurfaceVariant)),
73 + )
74 + ],
75 + ),
76 + ),
77 + ],
78 + ),
79 + ),
80 + );
81 + }
82 +}
lib/src/screens/nodes/widgets/node_list_row.dart
+71 -99
@@ -1,115 +1,55 @@
1 import 'package:cake_wallet/routes.dart';
2 -import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart';
3 -import 'package:cake_wallet/src/widgets/standard_list.dart';
2 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 +import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
4 import 'package:cw_core/node.dart';
5 import 'package:flutter/material.dart';
6
7 -class NodeListRow extends StandardListRow {
7 +class NodeListRow extends StatelessWidget {
8 NodeListRow(
9 - {required String title,
10 - String? subtitle,
11 - required this.node,
12 - required void Function(BuildContext context) onTap,
13 - required bool isSelected,
14 - required this.isPow})
15 - : super(title: title, onTap: onTap, isSelected: isSelected, subtitle: subtitle);
9 + {required this.node,
10 + required this.onTap,
11 + required this.onEditComplete,
12 + required this.isSelected,
13 + required this.isPow, this.speed});
14
15 final Node node;
16 final bool isPow;
17 + final VoidCallback onTap;
18 + final Future<void> Function(Object?) onEditComplete;
19 + final bool isSelected;
20 + final NodeSpeed? speed;
21
22 @override
23 Widget build(BuildContext context) {
24 final leading = buildLeading(context);
25 final trailing = buildTrailing(context);
24 - return Container(
25 - padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2),
26 - margin: EdgeInsets.only(top: 2, bottom: 2),
27 - child: FilledButton(
28 - onPressed: () => onTap?.call(context),
29 - style: FilledButton.styleFrom(
30 - backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
31 - shape: RoundedRectangleBorder(
32 - borderRadius: BorderRadius.all(Radius.circular(20)),
33 - ),
34 - ),
35 - child: Padding(
36 - padding: const EdgeInsets.symmetric(vertical: 8.0),
37 - child: Row(
38 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
39 - children: <Widget>[
40 - leading,
41 - buildCenter(context, hasLeftOffset: true),
42 - trailing,
43 - ],
44 - ),
45 - ),
46 - ),
47 - );
48 - }
49 -
50 - @override
51 - Widget buildLeading(BuildContext context) {
52 - return FutureBuilder(
53 - future: node.requestNode(),
54 - builder: (context, snapshot) {
55 - switch (snapshot.connectionState) {
56 - case ConnectionState.done:
57 - return NodeIndicator(isLive: snapshot.data ?? false);
58 - default:
59 - return NodeIndicator(isLive: false);
60 - }
61 - });
62 - }
26
64 - @override
65 - Widget buildTrailing(BuildContext context) {
27 + final hasLabel = node.label != null && node.label!.isNotEmpty;
28 + final uriText = "${node.uri.host}:${node.uri.port}";
29 return GestureDetector(
67 - onTap: () => Navigator.of(context).pushNamed(
68 - isPow ? Routes.newPowNode : Routes.newNode,
69 - arguments: {'editingNode': node, 'isSelected': isSelected},
70 - ),
71 - child: Container(
72 - padding: EdgeInsets.all(10),
73 - decoration: BoxDecoration(
74 - shape: BoxShape.circle,
75 - color: Theme.of(context).colorScheme.surface,
76 - ),
77 - child: Icon(
78 - Icons.edit,
79 - size: 14,
80 - color: Theme.of(context).colorScheme.onSurfaceVariant,
81 - ),
82 - ),
83 - );
84 - }
85 -}
86 -
87 -class NodeHeaderListRow extends StandardListRow {
88 - NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap})
89 - : super(title: title, onTap: onTap, isSelected: false);
90 -
91 - @override
92 - Widget build(BuildContext context) {
93 - final leading = buildLeading(context);
94 - final trailing = buildTrailing(context);
95 - return Container(
96 - height: 56,
97 - padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2),
98 - child: TextButton(
99 - onPressed: () => onTap?.call(context),
100 - style: ButtonStyle(
101 - backgroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
102 - shape: WidgetStateProperty.all(
103 - RoundedRectangleBorder(
104 - borderRadius: BorderRadius.all(Radius.circular(10)),
105 - ),
106 - ),
107 - ),
30 + behavior: HitTestBehavior.opaque,
31 + onTap: onTap,
32 + child: Padding(
33 + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 12),
34 child: Row(
35 + spacing: 12,
36 mainAxisAlignment: MainAxisAlignment.spaceBetween,
37 children: <Widget>[
111 - if (leading != null) leading,
112 - buildCenter(context, hasLeftOffset: leading != null),
38 + leading,
39 + Expanded(
40 + child: Column(crossAxisAlignment:CrossAxisAlignment.start,spacing:4,children: [
41 + Row(
42 + spacing: 4,
43 + mainAxisAlignment: MainAxisAlignment.start,
44 + children: [
45 + Text(hasLabel ? node.label! : uriText, style: TextStyle(fontSize: 12),),
46 + if(badgePath != null)
47 + CakeImageWidget(imageUrl: badgePath, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn),)
48 + ],
49 + ),
50 + if(hasLabel) Text(uriText, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),),
51 + ],),
52 + ),
53 trailing,
54 ],
55 ),
@@ -117,12 +57,44 @@ class NodeHeaderListRow extends StandardListRow {
57 );
58 }
59
120 - @override
60 + Widget buildLeading(BuildContext context) => isSelected
61 + ? Padding(
62 + padding: const EdgeInsets.all(4.0),
63 + child: Icon(
64 + Icons.check,
65 + size: 16,
66 + color: Theme.of(context).colorScheme.primary,
67 + ),
68 + )
69 + : SizedBox(width: 24, height: 24);
70 +
71 Widget buildTrailing(BuildContext context) {
122 - return SizedBox(
123 - width: 20,
124 - child: Icon(Icons.add,
125 - color: Theme.of(context).colorScheme.onSurfaceVariant, size: 24.0),
72 + return Row(
73 + children: [
74 + if (speed != null) CakeImageWidget(imageUrl: speed!.iconPath, width: 24, height: 24),
75 + GestureDetector(
76 + behavior: HitTestBehavior.opaque,
77 + onTap: () async {
78 + final res = await Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode,
79 + arguments: {'editingNode': node, 'isSelected': isSelected});
80 + await onEditComplete(res);
81 + },
82 + child: Padding(
83 + padding: const EdgeInsets.symmetric(horizontal: 8.0),
84 + child: CakeImageWidget(
85 + imageUrl: "assets/new-ui/3dots_vertical.svg",
86 + colorFilter:
87 + ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
88 + ),
89 + ),
90 + )
91 + ],
92 );
93 }
94 +
95 + String? get badgePath {
96 + if(node.isOfficial) return "assets/new-ui/official_node.svg";
97 + if(node.isBuiltin) return "assets/new-ui/default_node.svg";
98 + return null;
99 + }
100 }
lib/src/screens/settings/manage_nodes_page.dart
+93 -100
@@ -1,42 +1,42 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
3 import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
4 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
5 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6 import 'package:cake_wallet/routes.dart';
6 -import 'package:cake_wallet/src/screens/base_page.dart';
7 import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
8 -import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
8 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
10 -import 'package:cake_wallet/src/widgets/standard_list.dart';
11 -import 'package:cake_wallet/utils/feature_flag.dart';
9 import 'package:cake_wallet/utils/show_pop_up.dart';
10 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
11 import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
15 -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
12 +import 'package:cw_core/node.dart';
13 +import 'package:flutter/cupertino.dart';
14 import 'package:flutter/material.dart';
15 import 'package:flutter_mobx/flutter_mobx.dart';
16 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
17
20 -class ManageNodesPage extends BasePage {
21 - ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel, this.dashboardViewModel})
22 - : assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null)),
23 - assert(powNodeListViewModel == null || nodeListViewModel == null);
18 +class ManageNodesPage extends StatefulWidget {
19 + ManageNodesPage(this.isPow, {required this.nodeListViewModel, this.dashboardViewModel});
20
21 final DashboardViewModel? dashboardViewModel;
26 - final NodeListViewModel? nodeListViewModel;
27 - final PowNodeListViewModel? powNodeListViewModel;
22 + final NodeListViewModel nodeListViewModel;
23 final bool isPow;
24
25 @override
31 - bool get hideAppBar => true;
26 + State<ManageNodesPage> createState() => _ManageNodesPageState();
27 +}
28
29 +class _ManageNodesPageState extends State<ManageNodesPage> {
30 @override
34 - String get title => S.current.manage_nodes;
31 + void initState() {
32 + super.initState();
33 + Future.delayed(Duration(milliseconds: 300))
34 + .then((_) => widget.nodeListViewModel.speedTestNodes());
35 + }
36
37 @override
37 - Widget body(BuildContext context) {
38 + Widget build(BuildContext context) {
39 return ModalPageWrapper(
39 - horizontalPadding: 0,
40 topBar: ModalTopBar(
41 title: S.of(context).manage_nodes,
42 leadingIcon: Icon(Icons.arrow_back_ios_new),
@@ -44,87 +44,96 @@ class ManageNodesPage extends BasePage {
44 trailingWidget: Row(
45 spacing: 8,
46 children: [
47 - if (dashboardViewModel?.hasRescan ?? true)
48 - ModernButton(
47 + Observer(
48 + builder: (_) => ModernButton(
49 size: 36,
50 - icon: Icon(Icons.history),
51 - onPressed: () => Navigator.of(context).pushNamed(Routes.rescan)),
50 + icon: widget.nodeListViewModel.isTestingNodeSpeed
51 + ? CupertinoActivityIndicator()
52 + : Icon(Icons.refresh),
53 + onPressed: () => widget.nodeListViewModel.speedTestNodes()),
54 + ),
55 ModernButton(
56 size: 36,
57 icon: Icon(Icons.add),
55 - onPressed: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode))
58 + onPressed: ()async {
59 + final res = await Navigator.of(context)
60 + .pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
61 + if(res != null && res is Node) {
62 + widget.nodeListViewModel.nodes.add(res);
63 + }
64 + })
65 ],
66 ),
67 ),
59 - content: Column(
60 - children: [
61 - if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
62 - Observer(
63 - builder: (_) => SettingsSwitcherCell(
64 - key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'),
65 - title: S.current.enable_auto_node_switching,
66 - value: isPow
67 - ? powNodeListViewModel!.enableAutomaticNodeSwitching
68 - : nodeListViewModel!.enableAutomaticNodeSwitching,
69 - onValueChange: (BuildContext context, bool value) {
70 - if (isPow) {
71 - powNodeListViewModel!.setEnableAutomaticNodeSwitching(value);
72 - } else {
73 - nodeListViewModel!.setEnableAutomaticNodeSwitching(value);
68 + header: ModalHeader(
69 + iconPath: "assets/new-ui/settings_row_icons/nodes.svg",
70 + message: S.of(context).nodes_desc,
71 + title: S.of(context).nodes,
72 + bottomWidget: Observer(
73 + builder: (_) => NodeListRow(
74 + node: widget.nodeListViewModel.currentNode,
75 + speed: widget.nodeListViewModel.nodeSpeedFor(widget.nodeListViewModel.currentNode),
76 + onEditComplete: (res)async{
77 + if(res != null && res is Node) {
78 + widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
79 + widget.nodeListViewModel.nodes.add(res);
80 }
81 +
82 },
76 - ),
77 - ),
78 - if (FeatureFlag.isAutomaticNodeSwitchingEnabled) SizedBox(height: 8),
79 - Observer(
80 - builder: (BuildContext context) {
81 - int itemsCount =
82 - nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length;
83 - return SectionStandardList(
84 - scrollController: ModalScrollController.of(context),
85 - sectionCount: 1,
86 - shrinkWrap: true,
87 - physics: NeverScrollableScrollPhysics(),
88 - dividerPadding: EdgeInsets.symmetric(horizontal: 24),
89 - itemCounter: (int sectionIndex) => itemsCount,
90 - itemBuilder: (_, index) {
91 - return Observer(
92 - builder: (context) {
93 - final node =
94 - nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index];
95 - late bool isSelected;
96 - if (isPow) {
97 - isSelected = node.id == powNodeListViewModel!.currentNode.id;
98 - } else {
99 - isSelected = node.id == nodeListViewModel!.currentNode.id;
100 - }
101 - final nodeListRow = NodeListRow(
102 - title: node.uriRaw,
103 - subtitle: node.label ?? '',
104 - node: node,
105 - isSelected: isSelected,
106 - isPow: isPow,
107 - onTap: (_) async {
108 - if (isSelected) {
109 - return;
110 - }
83 + onTap: () {},
84 + isSelected: true,
85 + isPow: widget.isPow)),
86 + ),
87 + content: Column(
88 + children: [
89 + Container(
90 + decoration: BoxDecoration(
91 + color: Theme.of(context).colorScheme.surfaceContainer,
92 + borderRadius: BorderRadius.circular(18)),
93 + child: Observer(
94 + builder: (BuildContext context) {
95 + int itemsCount = widget.nodeListViewModel.nonCurrentNodes.length;
96 + return ListView.separated(
97 + controller: ModalScrollController.of(context),
98 + shrinkWrap: true,
99 + physics: BouncingScrollPhysics(),
100 + itemCount: itemsCount,
101 + separatorBuilder: (context, index) => Padding(
102 + padding: EdgeInsets.symmetric(horizontal: 12),
103 + child: Container(
104 + height: 1,
105 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
106 + ),
107 + ),
108 + itemBuilder: (_, index) {
109 + return Observer(
110 + builder: (context) {
111 + final node = widget.nodeListViewModel.nonCurrentNodes[index];
112 + final nodeListRow = NodeListRow(
113 + node: node,
114 + isSelected: false,
115 + isPow: widget.isPow,
116 + speed: widget.nodeListViewModel.nodeSpeedFor(node),
117 + onEditComplete: (res) async {
118 + if(res != null && res is Node) {
119 + widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
120 + widget.nodeListViewModel.nodes.add(res);
121 + }
122
123 + },
124 + onTap: () async {
125 await showPopUp<void>(
126 context: context,
127 builder: (BuildContext context) {
128 return AlertWithTwoActions(
129 alertTitle: S.of(context).change_current_node_title,
117 - alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ??
118 - powNodeListViewModel!.getAlertContent(node.uriRaw),
130 + alertContent:
131 + widget.nodeListViewModel.getAlertContent(node.uriRaw),
132 leftButtonText: S.of(context).cancel,
133 rightButtonText: S.of(context).change,
134 actionLeftButton: () => Navigator.of(context).pop(),
135 actionRightButton: () async {
123 - if (isPow) {
124 - await powNodeListViewModel!.setAsCurrent(node);
125 - } else {
126 - await nodeListViewModel!.setAsCurrent(node);
127 - }
136 + await widget.nodeListViewModel.setAsCurrent(node);
137 Navigator.of(context).pop();
138 },
139 );
@@ -132,32 +141,16 @@ class ManageNodesPage extends BasePage {
141 );
142 },
143 );
135 - return nodeListRow;
136 - },
137 - );
138 - },
139 - );
140 - },
144 + return nodeListRow;
145 + },
146 + );
147 + },
148 + );
149 + },
150 + ),
151 ),
152 ],
153 ),
154 );
155 }
146 - Future<void> _presentReconnectAlert(BuildContext context) async {
147 - await showPopUp<void>(
148 - context: context,
149 - builder: (BuildContext context) {
150 - return AlertWithTwoActions(
151 - alertTitle: S.of(context).reconnection,
152 - alertContent: S.of(context).reconnect_alert_text,
153 - rightButtonText: S.of(context).ok,
154 - leftButtonText: S.of(context).cancel,
155 - actionRightButton: () async {
156 - Navigator.of(context).pop();
157 - await dashboardViewModel!.reconnect();
158 - },
159 - actionLeftButton: () => Navigator.of(context).pop());
160 - },
161 - );
162 - }
156 }
lib/view_model/node_list/node_create_or_edit_view_model.dart
+1 -18
@@ -3,10 +3,7 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
3 import 'package:cake_wallet/entities/qr_scanner.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 import 'package:cake_wallet/utils/permission_handler.dart';
6 -import 'package:collection/collection.dart';
6 import 'package:cw_core/node.dart';
8 -import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
9 -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
7 import 'package:cw_core/utils/proxy_wrapper.dart';
8 import 'package:cw_core/wallet_type.dart';
9 import 'package:flutter/cupertino.dart';
@@ -20,7 +17,7 @@ class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase
17
18 abstract class NodeCreateOrEditViewModelBase with Store {
19 NodeCreateOrEditViewModelBase(
23 - this.isPow, this.nodeListViewModel,this.powNodeListViewModel, this.walletType, this._settingsStore, {this.editingNode})
20 + this.isPow, this.walletType, this._settingsStore, {this.editingNode})
21 : state = InitialExecutionState(),
22 connectionState = InitialExecutionState(),
23 label = editingNode?.label ?? '',
@@ -116,8 +113,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
113 @observable
114 bool isPow;
115
119 - final NodeListViewModel? nodeListViewModel;
120 - final PowNodeListViewModel? powNodeListViewModel;
116
117 @computed
118 bool get isReady =>
@@ -257,12 +252,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
252 @action
253 Future<void> delete({required Node editingNode}) async {
254 await editingNode.delete();
260 - if(nodeListViewModel != null) {
261 - nodeListViewModel!.bindNodes();
262 - }
263 - if(powNodeListViewModel != null) {
264 - powNodeListViewModel!.bindNodes();
265 - }
255 }
256
257 @action
@@ -287,12 +276,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
276 }
277
278 state = ExecutedSuccessfullyState();
290 - if(nodeListViewModel != null) {
291 - nodeListViewModel!.bindNodes();
292 - }
293 - if(powNodeListViewModel != null) {
294 - powNodeListViewModel!.bindNodes();
295 - }
279
280 } catch (e) {
281 state = FailureState(e.toString());
lib/view_model/node_list/node_list_view_model.dart
+89 -5
@@ -1,12 +1,14 @@
1 +import 'dart:math';
2 +
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/store/app_store.dart';
5 import 'package:cw_core/node_list.dart';
6 +import 'package:cw_core/utils/print_verbose.dart';
7 import 'package:cw_core/utils/proxy_wrapper.dart';
8 import 'package:mobx/mobx.dart';
9 import 'package:cw_core/wallet_base.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
11 import 'package:cw_core/node.dart';
9 -import 'package:cake_wallet/entities/node_list.dart';
12 import 'package:cake_wallet/entities/default_settings_migration.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:cake_wallet/evm/evm.dart';
@@ -14,11 +16,34 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
16
17 part 'node_list_view_model.g.dart';
18
19 +
20 +class NodeSpeed {
21 + final String iconPath;
22 +
23 + // fraction of best node ping needed to be classified as this speed.
24 + final double treshold;
25 +
26 + const NodeSpeed._(this.iconPath, this.treshold);
27 +
28 + static const disconnected = NodeSpeed._(
29 + "assets/new-ui/node_speed_badges/disconnected.svg", 0);
30 + static const slow = NodeSpeed._(
31 + "assets/new-ui/node_speed_badges/slow.svg", 0);
32 + static const medium = NodeSpeed._(
33 + "assets/new-ui/node_speed_badges/medium.svg", 0.5);
34 + static const fast = NodeSpeed._(
35 + "assets/new-ui/node_speed_badges/fast.svg", 0.8);
36 +
37 + static const all = [fast, medium, slow, disconnected];
38 +}
39 +
40 +
41 class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel;
42
43 abstract class NodeListViewModelBase with Store {
20 - NodeListViewModelBase(this._appStore)
44 + NodeListViewModelBase(this._appStore, this.isPow)
45 : nodes = ObservableList<Node>(),
46 + _nodeSpeeds = ObservableMap<String, int>(),
47 settingsStore = _appStore.settingsStore {
48 bindNodes();
49
@@ -37,6 +62,7 @@ abstract class NodeListViewModelBase with Store {
62 bindNodes();
63 });
64 }
65 +
66
67 @computed
68 Node get currentNode {
@@ -52,7 +78,7 @@ abstract class NodeListViewModelBase with Store {
78 return settingsStore.getCurrentNode(walletType, chainId: chainId);
79 }
80
55 - final node = settingsStore.nodes[walletType];
81 + final node = isPow ? settingsStore.powNodes[walletType] : settingsStore.nodes[walletType];
82 if (node == null) {
83 throw Exception('No node for wallet type: $walletType');
84 }
@@ -73,6 +99,59 @@ abstract class NodeListViewModelBase with Store {
99 final ObservableList<Node> nodes;
100 final SettingsStore settingsStore;
101 final AppStore _appStore;
102 + final bool isPow;
103 + final ObservableMap<String, int> _nodeSpeeds;
104 +
105 +
106 + @computed
107 + List<Node> get nonCurrentNodes => nodes.where((item)=>item != currentNode).toList();
108 +
109 + @observable
110 + bool isTestingNodeSpeed = false;
111 +
112 + Future<void> speedTestNodes() async {
113 + try {
114 + isTestingNodeSpeed = true;
115 + _nodeSpeeds.clear();
116 + final nodes = this.nodes.toList();
117 + await Future.wait(nodes.map((node) async {
118 + final sw = Stopwatch()..start();
119 + final res = await node
120 + .requestNode()
121 + .timeout(const Duration(seconds: 10), onTimeout: () => false);
122 + sw.stop();
123 + if (res) {
124 + _nodeSpeeds[node.uriRaw] = sw.elapsedMilliseconds;
125 + }
126 + }));
127 + } finally {
128 + isTestingNodeSpeed = false;
129 + }
130 + }
131 +
132 + NodeSpeed? nodeSpeedFor(Node node) {
133 + final bestSpeed = _bestNodeSpeed;
134 + final currentSpeed = _nodeSpeeds[node.uriRaw];
135 + if (bestSpeed == null) {
136 + return null;
137 + }
138 + if (currentSpeed == null) {
139 + return NodeSpeed.disconnected;
140 + }
141 + for (final speed in NodeSpeed.all) {
142 + if (currentSpeed < (bestSpeed / speed.treshold)) return speed;
143 + }
144 +
145 + return null;
146 + }
147 +
148 + int? get _bestNodeSpeed {
149 + try {
150 + printV(_nodeSpeeds);
151 + return _nodeSpeeds.values.reduce(min);
152 + } catch (e) {}
153 + return null;
154 + }
155
156 Future<void> reset() async {
157 await resetToDefault();
@@ -125,7 +204,12 @@ abstract class NodeListViewModelBase with Store {
204 }
205
206 // For non-EVM wallets, use the wallet type directly
128 - settingsStore.nodes[walletType] = node;
207 + if(isPow) {
208 + settingsStore.powNodes[walletType] = node;
209 + } else {
210 + settingsStore.nodes[walletType] = node;
211 + }
212 +
213 }
214
215 @action
@@ -150,7 +234,7 @@ abstract class NodeListViewModelBase with Store {
234 }
235
236 // For non-EVM wallets, use the wallet type directly
153 - nodes.addAll(await Node.getAllForWalletType(walletType));
237 + nodes.addAll(isPow ? await Node.getAllForWalletTypePow(walletType) : await Node.getAllForWalletType(walletType));
238
239 }
240 }
lib/view_model/node_list/pow_node_list_view_model.dart
-83
@@ -1,83 +0,0 @@
1 -import 'package:cake_wallet/generated/i18n.dart';
2 -import 'package:cake_wallet/store/app_store.dart';
3 -import 'package:cake_wallet/utils/mobx.dart';
4 -import 'package:cw_core/node_list.dart';
5 -import 'package:cw_core/utils/proxy_wrapper.dart';
6 -import 'package:hive/hive.dart';
7 -import 'package:mobx/mobx.dart';
8 -import 'package:cw_core/wallet_base.dart';
9 -import 'package:cake_wallet/store/settings_store.dart';
10 -import 'package:cw_core/node.dart';
11 -import 'package:cake_wallet/entities/node_list.dart';
12 -import 'package:cake_wallet/entities/default_settings_migration.dart';
13 -import 'package:cw_core/wallet_type.dart';
14 -
15 -part 'pow_node_list_view_model.g.dart';
16 -
17 -class PowNodeListViewModel = PowNodeListViewModelBase with _$PowNodeListViewModel;
18 -
19 -abstract class PowNodeListViewModelBase with Store {
20 - PowNodeListViewModelBase(this._appStore)
21 - : nodes = ObservableList<Node>(),
22 - settingsStore = _appStore.settingsStore {
23 - bindNodes();
24 -
25 - reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
26 - bindNodes();
27 - });
28 - }
29 -
30 - @computed
31 - Node get currentNode {
32 - final node = settingsStore.powNodes[_appStore.wallet!.type];
33 -
34 - if (node == null) {
35 - throw Exception('No node for wallet type: ${_appStore.wallet!.type}');
36 - }
37 -
38 - return node;
39 - }
40 -
41 - String getAlertContent(String uri) =>
42 - S.current.change_current_node(uri) +
43 - '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance!.enabled ? '' : S.current.orbot_running_alert) : ''}';
44 -
45 - @computed
46 - bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
47 -
48 - @action
49 - void setEnableAutomaticNodeSwitching(bool value) =>
50 - settingsStore.enableAutomaticNodeSwitching = value;
51 -
52 - final ObservableList<Node> nodes;
53 - final SettingsStore settingsStore;
54 - final AppStore _appStore;
55 -
56 - Future<void> reset() async {
57 - await resetPowToDefault();
58 -
59 - Node node;
60 -
61 - switch (_appStore.wallet!.type) {
62 - case WalletType.nano:
63 - node = (await getNanoDefaultPowNode())!;
64 - break;
65 - default:
66 - throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
67 - }
68 -
69 - await setAsCurrent(node);
70 - }
71 -
72 - @action
73 - Future<void> delete(Node node) async => node.delete();
74 -
75 - Future<void> setAsCurrent(Node node) async =>
76 - settingsStore.powNodes[_appStore.wallet!.type] = node;
77 -
78 - @action
79 - Future<void> bindNodes() async {
80 - nodes.clear();
81 - nodes.addAll(await Node.getAllPow());
82 - }
83 -}
pubspec_base.yaml
+1
@@ -290,6 +290,7 @@ flutter:
290 - assets/new-ui/card_icons/outline_icons/
291 - assets/new-ui/card_icons/symbol_icons/
292 - assets/new-ui/crypto_full_icons/
293 + - assets/new-ui/node_speed_badges/
294
295 fonts:
296 - family: Lato
res/pictures/default_node.svg new
+3
@@ -0,0 +1,3 @@
1 +<svg width="8" height="10" viewBox="0 0 8 10" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M3.475 5.275L2.775 4.575C2.675 4.475 2.55833 4.425 2.425 4.425C2.29167 4.425 2.175 4.475 2.075 4.575C1.975 4.675 1.925 4.79375 1.925 4.93125C1.925 5.06875 1.975 5.1875 2.075 5.2875L3.125 6.35C3.225 6.45 3.34167 6.5 3.475 6.5C3.60833 6.5 3.725 6.45 3.825 6.35L5.95 4.225C6.05 4.125 6.1 4.00625 6.1 3.86875C6.1 3.73125 6.05 3.6125 5.95 3.5125C5.85 3.4125 5.73125 3.3625 5.59375 3.3625C5.45625 3.3625 5.3375 3.4125 5.2375 3.5125L3.475 5.275ZM3.8375 9.8625C3.7875 9.85417 3.7375 9.84167 3.6875 9.825C2.5625 9.45 1.66667 8.75625 1 7.74375C0.333333 6.73125 0 5.64167 0 4.475V2.1125C0 1.90417 0.0604167 1.71667 0.18125 1.55C0.302083 1.38333 0.458333 1.2625 0.65 1.1875L3.65 0.0625C3.76667 0.0208333 3.88333 0 4 0C4.11667 0 4.23333 0.0208333 4.35 0.0625L7.35 1.1875C7.54167 1.2625 7.69792 1.38333 7.81875 1.55C7.93958 1.71667 8 1.90417 8 2.1125V4.475C8 5.64167 7.66667 6.73125 7 7.74375C6.33333 8.75625 5.4375 9.45 4.3125 9.825C4.2625 9.84167 4.2125 9.85417 4.1625 9.8625C4.1125 9.87083 4.05833 9.875 4 9.875C3.94167 9.875 3.8875 9.87083 3.8375 9.8625Z" fill="white"/>
3 +</svg>
res/pictures/node_speed_badges/disconnected.svg new
+4
@@ -0,0 +1,4 @@
1 +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625ZM17.4375 19.5625C17.1458 19.2708 17 18.9167 17 18.5V5.5C17 5.08333 17.1458 4.72917 17.4375 4.4375C17.7292 4.14583 18.0833 4 18.5 4C18.9167 4 19.2708 4.14583 19.5625 4.4375C19.8542 4.72917 20 5.08333 20 5.5V18.5C20 18.9167 19.8542 19.2708 19.5625 19.5625C19.2708 19.8542 18.9167 20 18.5 20C18.0833 20 17.7292 19.8542 17.4375 19.5625Z" fill="#FFB4AB" fill-opacity="0.5"/>
3 + <path d="M16.4916 18.4367L14.6743 20.2178C14.4363 20.4552 14.1388 20.5794 13.7818 20.5902C13.4249 20.601 13.1166 20.4768 12.857 20.2178C12.619 19.9803 12.5 19.678 12.5 19.311C12.5 18.944 12.619 18.6418 12.857 18.4043L14.6743 16.5908L12.857 14.7773C12.619 14.5399 12.5 14.2376 12.5 13.8706C12.5 13.5036 12.619 13.2014 12.857 12.9639C13.0949 12.7264 13.3978 12.6077 13.7656 12.6077C14.1334 12.6077 14.4363 12.7264 14.6743 12.9639L16.4916 14.7773L18.3089 12.9639C18.5468 12.7264 18.8443 12.6023 19.2013 12.5915C19.5583 12.5807 19.8665 12.7048 20.1262 12.9639C20.3641 13.2014 20.4831 13.5036 20.4831 13.8706C20.4831 14.2376 20.3641 14.5399 20.1262 14.7773L18.3413 16.5908L20.1262 18.4043C20.3641 18.6418 20.4885 18.9386 20.4994 19.2948C20.5102 19.6511 20.3858 19.9587 20.1262 20.2178C19.8882 20.4552 19.5853 20.574 19.2175 20.574C18.8497 20.574 18.5468 20.4552 18.3089 20.2178L16.4916 18.4367Z" fill="white"/>
4 +</svg>
res/pictures/node_speed_badges/fast.svg new
+3
@@ -0,0 +1,3 @@
1 +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625ZM17.4375 19.5625C17.1458 19.2708 17 18.9167 17 18.5V5.5C17 5.08333 17.1458 4.72917 17.4375 4.4375C17.7292 4.14583 18.0833 4 18.5 4C18.9167 4 19.2708 4.14583 19.5625 4.4375C19.8542 4.72917 20 5.08333 20 5.5V18.5C20 18.9167 19.8542 19.2708 19.5625 19.5625C19.2708 19.8542 18.9167 20 18.5 20C18.0833 20 17.7292 19.8542 17.4375 19.5625Z" fill="#4DA865"/>
3 +</svg>
res/pictures/node_speed_badges/medium.svg new
+4
@@ -0,0 +1,4 @@
1 +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625Z" fill="#FAC06A"/>
3 + <path d="M17 18.5C17 18.9167 17.1458 19.2708 17.4375 19.5625C17.7292 19.8542 18.0833 20 18.5 20C18.9167 20 19.2708 19.8542 19.5625 19.5625C19.8542 19.2708 20 18.9167 20 18.5V5.5C20 5.08333 19.8542 4.72917 19.5625 4.4375C19.2708 4.14583 18.9167 4 18.5 4C18.0833 4 17.7292 4.14583 17.4375 4.4375C17.1458 4.72917 17 5.08333 17 5.5V18.5Z" fill="#FAC06A" fill-opacity="0.5"/>
4 +</svg>
res/pictures/node_speed_badges/slow.svg new
+7
@@ -0,0 +1,7 @@
1 +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625Z" fill="#FFB4AB"/>
3 + <g opacity="0.5">
4 + <path d="M11 18.5C11 18.9167 11.1458 19.2708 11.4375 19.5625C11.7292 19.8542 12.0833 20 12.5 20C12.9167 20 13.2708 19.8542 13.5625 19.5625C13.8542 19.2708 14 18.9167 14 18.5V10.5C14 10.0833 13.8542 9.72917 13.5625 9.4375C13.2708 9.14583 12.9167 9 12.5 9C12.0833 9 11.7292 9.14583 11.4375 9.4375C11.1458 9.72917 11 10.0833 11 10.5V18.5Z" fill="#FFB4AB"/>
5 + <path d="M17 18.5C17 18.9167 17.1458 19.2708 17.4375 19.5625C17.7292 19.8542 18.0833 20 18.5 20C18.9167 20 19.2708 19.8542 19.5625 19.5625C19.8542 19.2708 20 18.9167 20 18.5V5.5C20 5.08333 19.8542 4.72917 19.5625 4.4375C19.2708 4.14583 18.9167 4 18.5 4C18.0833 4 17.7292 4.14583 17.4375 4.4375C17.1458 4.72917 17 5.08333 17 5.5V18.5Z" fill="#FFB4AB"/>
6 + </g>
7 +</svg>
res/pictures/official_node.svg new
+3
@@ -0,0 +1,3 @@
1 +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2 + <path d="M5.21354 5.92351L4.42812 5.15164C4.32882 5.05233 4.20469 5.00268 4.05573 5.00268C3.90677 5.00268 3.77813 5.05685 3.66979 5.16518C3.57049 5.26448 3.52083 5.39087 3.52083 5.54434C3.52083 5.69782 3.57049 5.82421 3.66979 5.92351L4.83437 7.0881C4.94271 7.19643 5.0691 7.2506 5.21354 7.2506C5.35799 7.2506 5.48438 7.19643 5.59271 7.0881L7.89479 4.78601C8.00313 4.67768 8.05503 4.55129 8.05052 4.40684C8.04601 4.2624 7.9941 4.13601 7.89479 4.02768C7.78646 3.91934 7.65781 3.86292 7.50885 3.85841C7.3599 3.85389 7.23125 3.9058 7.12292 4.01414L5.21354 5.92351ZM3.69688 10.8256L2.91146 9.49851L1.42188 9.17351C1.28646 9.14643 1.17812 9.07646 1.09687 8.96362C1.01562 8.85077 0.984028 8.72664 1.00208 8.59122L1.15104 7.06101L0.135417 5.89643C0.0451389 5.79712 0 5.67976 0 5.54434C0 5.40893 0.0451389 5.29157 0.135417 5.19226L1.15104 4.02768L1.00208 2.49747C0.984028 2.36205 1.01562 2.23792 1.09687 2.12507C1.17812 2.01223 1.28646 1.94226 1.42188 1.91518L2.91146 1.59018L3.69688 0.263095C3.7691 0.145734 3.8684 0.0667411 3.99479 0.0261161C4.12118 -0.0145089 4.24757 -0.00773809 4.37396 0.0464286L5.78229 0.642262L7.19063 0.0464286C7.31701 -0.00773809 7.4434 -0.0145089 7.56979 0.0261161C7.69618 0.0667411 7.79549 0.145734 7.86771 0.263095L8.65312 1.59018L10.1427 1.91518C10.2781 1.94226 10.3865 2.01223 10.4677 2.12507C10.549 2.23792 10.5806 2.36205 10.5625 2.49747L10.4135 4.02768L11.4292 5.19226C11.5194 5.29157 11.5646 5.40893 11.5646 5.54434C11.5646 5.67976 11.5194 5.79712 11.4292 5.89643L10.4135 7.06101L10.5625 8.59122C10.5806 8.72664 10.549 8.85077 10.4677 8.96362C10.3865 9.07646 10.2781 9.14643 10.1427 9.17351L8.65312 9.49851L7.86771 10.8256C7.79549 10.943 7.69618 11.0219 7.56979 11.0626C7.4434 11.1032 7.31701 11.0964 7.19063 11.0423L5.78229 10.4464L4.37396 11.0423C4.24757 11.0964 4.12118 11.1032 3.99479 11.0626C3.8684 11.0219 3.7691 10.943 3.69688 10.8256Z" fill="white"/>
3 +</svg>
res/values/strings_en.arb
+4
@@ -300,6 +300,7 @@
300 "default_sell_provider": "Default Sell Provider",
301 "delete": "Delete",
302 "delete_account": "Delete Account",
303 + "delete_this_node": "Delete this node",
304 "delete_wallet": "Delete wallet",
305 "delete_wallet_confirm_message": "Are you sure that you want to delete ${wallet_name} wallet?",
306 "deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to",
@@ -681,8 +682,10 @@
682 "node_new": "New Node",
683 "node_port": "Node port",
684 "node_reset_settings_title": "Reset settings",
685 + "node_share_desc": "Scan this QR code from the Scan screen to import this node.",
686 "node_test": "Test",
687 "nodes": "Nodes",
688 + "nodes_desc": "Manage existing nodes or add a custom one.",
689 "nodes_list_reset_to_default_message": "Are you sure that you want to reset settings to default?",
690 "none_of_selected_providers_can_exchange": "None of the selected providers can make this swap",
691 "noNFTYet": "No NFTs yet",
@@ -1022,6 +1025,7 @@
1025 "share_address": "Share address",
1026 "shared_seed_wallet_groups": "Shared Seed Wallet Groups",
1027 "shared_seed_wallets": "Shared Seed Wallets",
1028 + "share_this_node": "Share this node",
1029 "show": "Show",
1030 "show_address_book_popup": "Show Address Book popup",
1031 "show_balance": "Long Press to Show Balance",