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>
This commit is contained in:
malik1004x
2026-06-01 21:32:12 +02:00
committed by GitHub
co-authored by Omar Hatem
parent cfe332a878
commit 6c1ee16513
20 changed files with 472 additions and 404 deletions
+3 -13
View File
@@ -1118,9 +1118,9 @@ Future<void> setup({
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
getIt.registerLazySingleton(() {
getIt.registerFactoryParam<NodeListViewModel, bool, void>((isPow, _) {
final appStore = getIt.get<AppStore>();
return NodeListViewModel(appStore);
return NodeListViewModel(appStore, isPow);
});
getIt.registerFactoryParam<NewAddressesPage, bool, void>(
@@ -1131,11 +1131,6 @@ Future<void> setup({
),
);
getIt.registerLazySingleton(() {
final appStore = getIt.get<AppStore>();
return PowNodeListViewModel(appStore);
});
getIt.registerFactory(() => ConnectionSyncViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<ConnectionSyncViewModel>()));
@@ -1178,8 +1173,6 @@ Future<void> setup({
final Node? editingNode = args['editingNode'] as Node?;
return NodeCreateOrEditViewModel(
isPow,
getIt.get<NodeListViewModel>(),
getIt.get<PowNodeListViewModel>(),
type,
getIt.get<SettingsStore>(),
editingNode: editingNode
@@ -1700,10 +1693,7 @@ Future<void> setup({
);
getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
if (isPow) {
return ManageNodesPage(isPow, powNodeListViewModel: getIt.get<PowNodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
}
return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(param1: isPow), dashboardViewModel: getIt.get<DashboardViewModel>());
});
getIt.registerFactory(
@@ -17,10 +17,10 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import '../../../pages/receive_page.dart';
@@ -198,7 +198,12 @@ class CoinActionRow extends StatelessWidget {
Navigator.of(context)
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
return;
} else {
} else if(["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
Navigator.of(context).pushNamed(Routes.newNode,
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
return;
}else {
final uri = Uri.tryParse(code);
if (uri == null) return;
req = PaymentRequest.fromUri(uri);
+7 -2
View File
@@ -4,11 +4,12 @@ import 'package:flutter_svg/svg.dart';
class ModalHeader extends StatelessWidget {
const ModalHeader(
{super.key, required this.iconPath, required this.message, required this.title});
{super.key, required this.iconPath, required this.message, required this.title, this.bottomWidget});
final String iconPath;
final String title;
final String message;
final Widget? bottomWidget;
@override
Widget build(BuildContext context) {
@@ -41,7 +42,11 @@ class ModalHeader extends StatelessWidget {
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
))
)),
if(bottomWidget != null) ...[
Padding(padding: EdgeInsets.symmetric(horizontal: 18), child: Container(height:1, color: Theme.of(context).colorScheme.surfaceContainerHigh),),
bottomWidget!
]
],
),
),
@@ -1,20 +1,20 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/screens/nodes/node_share_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mobx/mobx.dart';
class NodeCreateOrEditPage extends StatefulWidget {
@@ -76,6 +76,14 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
},
);
reaction((_)=>widget.nodeCreateOrEditViewModel.state, (state) async {
if(state is ExecutedSuccessfullyState) {
await Future.delayed(Duration(milliseconds: 100));
if (mounted) Navigator.of(context).pop(widget.nodeCreateOrEditViewModel.editingNode);
}
});
}
@override
@@ -90,86 +98,62 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
trailingIcon: CakeImageWidget(imageUrl:
"assets/new-ui/scan.svg",width:24,height:24,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
trailingIcon: Icon(Icons.check),
onTrailingPressed: () async {
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
return;
}
await widget.nodeCreateOrEditViewModel
.save(saveAsCurrent: widget.isSelected ?? false);
},
),
Expanded(
child: KeyboardHideOverlay(
child: Container(
padding: const EdgeInsets.only(left: 18, right: 18),
child: ScrollableWithBottomSection(
contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
content: NodeForm(
child: Column(
spacing: 18,
children: [NodeForm(
key: _nodeFormKey,
nodeViewModel: widget.nodeCreateOrEditViewModel,
),
bottomSectionPadding: const EdgeInsets.only(bottom: 24),
bottomSection: Observer(
builder: (_) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
onPressed: () async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).remove_node,
alertContent: S.of(context).remove_node_message,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.pop(context, true),
actionLeftButton: () => Navigator.pop(context, false),
),
) ??
false;
NewListSections(sections: {"":[
if(widget.editingNode != null)
if (confirmed) {
await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
Navigator.of(context).pop();
}
},
text: S.of(context).delete,
isDisabled: widget.editingNode == null ||
!widget.nodeCreateOrEditViewModel.isReady ||
widget.editingNode!.isBuiltin ||
(widget.isSelected ?? false),
color: Theme.of(context).colorScheme.errorContainer,
textColor: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 8.0),
child: PrimaryButton(
onPressed: () async {
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
return;
}
ListItemRegularRow(keyValue: "share", label: S.of(context).share_this_node, foregroundColor: Theme.of(context).colorScheme.primary, showArrow: false, onTap: (){
await widget.nodeCreateOrEditViewModel.save(
saveAsCurrent: widget.isSelected ?? false);
if (context.mounted) {
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) ||
(widget.nodeCreateOrEditViewModel.connectionState is IsExecutingState),
),
),
),
],
),
),
Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>NodeSharePage(uri: widget.editingNode!.uri, currency: walletTypeToCryptoCurrency(widget.type!))));
}),
if(!(widget.editingNode == null ||
!widget.nodeCreateOrEditViewModel.isReady ||
widget.editingNode!.isBuiltin ||
(widget.isSelected ?? false)))
ListItemRegularRow(keyValue: "delete", label: S.of(context).delete_this_node, foregroundColor: Theme.of(context).colorScheme.errorContainer, showArrow: false, onTap: ()async{
final confirmed = await showPopUp<bool>(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).remove_node,
alertContent: S.of(context).remove_node_message,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.pop(context, true),
actionLeftButton: () => Navigator.pop(context, false),
),
) ??
false;
if (confirmed) {
await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
Navigator.of(context).pop();
}
})
]})
]
),
),
),
@@ -0,0 +1,82 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class NodeSharePage extends StatelessWidget {
const NodeSharePage({super.key, required this.uri, required this.currency});
final Uri uri;
final CryptoCurrency currency;
@override
Widget build(BuildContext context) {
final qrSize = MediaQuery.of(context).size.width * 0.786;
final padding = 16.0;
return Container(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
ModalTopBar(
title: S.of(context).share,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 40,
children: [
Stack(alignment: Alignment.center, children: [
SizedBox(
width: qrSize,
height: qrSize,
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: QrImage(
size: qrSize - padding * 2,
embeddedImagePath: "",
data: uri.toString())),
),
SizedBox(
width: 60,
height: 60,
child: Stack(
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/nodes.svg",
width: 54,
height: 54,
),
Align(
alignment: Alignment.bottomRight,
child: CakeImageWidget(
imageUrl: currency.iconPath,
width: 24,
height: 24,
))
],
),
),
]),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 36.0),
child: Text(S.of(context).node_share_desc,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
)
],
),
),
],
),
),
);
}
}
@@ -1,115 +1,55 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/material.dart';
class NodeListRow extends StandardListRow {
class NodeListRow extends StatelessWidget {
NodeListRow(
{required String title,
String? subtitle,
required this.node,
required void Function(BuildContext context) onTap,
required bool isSelected,
required this.isPow})
: super(title: title, onTap: onTap, isSelected: isSelected, subtitle: subtitle);
{required this.node,
required this.onTap,
required this.onEditComplete,
required this.isSelected,
required this.isPow, this.speed});
final Node node;
final bool isPow;
final VoidCallback onTap;
final Future<void> Function(Object?) onEditComplete;
final bool isSelected;
final NodeSpeed? speed;
@override
Widget build(BuildContext context) {
final leading = buildLeading(context);
final trailing = buildTrailing(context);
return Container(
padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2),
margin: EdgeInsets.only(top: 2, bottom: 2),
child: FilledButton(
onPressed: () => onTap?.call(context),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
leading,
buildCenter(context, hasLeftOffset: true),
trailing,
],
),
),
),
);
}
@override
Widget buildLeading(BuildContext context) {
return FutureBuilder(
future: node.requestNode(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return NodeIndicator(isLive: snapshot.data ?? false);
default:
return NodeIndicator(isLive: false);
}
});
}
@override
Widget buildTrailing(BuildContext context) {
final hasLabel = node.label != null && node.label!.isNotEmpty;
final uriText = "${node.uri.host}:${node.uri.port}";
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(
isPow ? Routes.newPowNode : Routes.newNode,
arguments: {'editingNode': node, 'isSelected': isSelected},
),
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}
}
class NodeHeaderListRow extends StandardListRow {
NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap})
: super(title: title, onTap: onTap, isSelected: false);
@override
Widget build(BuildContext context) {
final leading = buildLeading(context);
final trailing = buildTrailing(context);
return Container(
height: 56,
padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2),
child: TextButton(
onPressed: () => onTap?.call(context),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 12),
child: Row(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
if (leading != null) leading,
buildCenter(context, hasLeftOffset: leading != null),
leading,
Expanded(
child: Column(crossAxisAlignment:CrossAxisAlignment.start,spacing:4,children: [
Row(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(hasLabel ? node.label! : uriText, style: TextStyle(fontSize: 12),),
if(badgePath != null)
CakeImageWidget(imageUrl: badgePath, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn),)
],
),
if(hasLabel) Text(uriText, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),),
],),
),
trailing,
],
),
@@ -117,12 +57,44 @@ class NodeHeaderListRow extends StandardListRow {
);
}
@override
Widget buildLeading(BuildContext context) => isSelected
? Padding(
padding: const EdgeInsets.all(4.0),
child: Icon(
Icons.check,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
)
: SizedBox(width: 24, height: 24);
Widget buildTrailing(BuildContext context) {
return SizedBox(
width: 20,
child: Icon(Icons.add,
color: Theme.of(context).colorScheme.onSurfaceVariant, size: 24.0),
return Row(
children: [
if (speed != null) CakeImageWidget(imageUrl: speed!.iconPath, width: 24, height: 24),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
final res = await Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode,
arguments: {'editingNode': node, 'isSelected': isSelected});
await onEditComplete(res);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CakeImageWidget(
imageUrl: "assets/new-ui/3dots_vertical.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
),
)
],
);
}
String? get badgePath {
if(node.isOfficial) return "assets/new-ui/official_node.svg";
if(node.isBuiltin) return "assets/new-ui/default_node.svg";
return null;
}
}
+95 -102
View File
@@ -1,42 +1,42 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class ManageNodesPage extends BasePage {
ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel, this.dashboardViewModel})
: assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null)),
assert(powNodeListViewModel == null || nodeListViewModel == null);
class ManageNodesPage extends StatefulWidget {
ManageNodesPage(this.isPow, {required this.nodeListViewModel, this.dashboardViewModel});
final DashboardViewModel? dashboardViewModel;
final NodeListViewModel? nodeListViewModel;
final PowNodeListViewModel? powNodeListViewModel;
final NodeListViewModel nodeListViewModel;
final bool isPow;
@override
bool get hideAppBar => true;
State<ManageNodesPage> createState() => _ManageNodesPageState();
}
class _ManageNodesPageState extends State<ManageNodesPage> {
@override
void initState() {
super.initState();
Future.delayed(Duration(milliseconds: 300))
.then((_) => widget.nodeListViewModel.speedTestNodes());
}
@override
String get title => S.current.manage_nodes;
@override
Widget body(BuildContext context) {
Widget build(BuildContext context) {
return ModalPageWrapper(
horizontalPadding: 0,
topBar: ModalTopBar(
title: S.of(context).manage_nodes,
leadingIcon: Icon(Icons.arrow_back_ios_new),
@@ -44,87 +44,96 @@ class ManageNodesPage extends BasePage {
trailingWidget: Row(
spacing: 8,
children: [
if (dashboardViewModel?.hasRescan ?? true)
ModernButton(
Observer(
builder: (_) => ModernButton(
size: 36,
icon: Icon(Icons.history),
onPressed: () => Navigator.of(context).pushNamed(Routes.rescan)),
icon: widget.nodeListViewModel.isTestingNodeSpeed
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
onPressed: () => widget.nodeListViewModel.speedTestNodes()),
),
ModernButton(
size: 36,
icon: Icon(Icons.add),
onPressed: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode))
onPressed: ()async {
final res = await Navigator.of(context)
.pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
if(res != null && res is Node) {
widget.nodeListViewModel.nodes.add(res);
}
})
],
),
),
header: ModalHeader(
iconPath: "assets/new-ui/settings_row_icons/nodes.svg",
message: S.of(context).nodes_desc,
title: S.of(context).nodes,
bottomWidget: Observer(
builder: (_) => NodeListRow(
node: widget.nodeListViewModel.currentNode,
speed: widget.nodeListViewModel.nodeSpeedFor(widget.nodeListViewModel.currentNode),
onEditComplete: (res)async{
if(res != null && res is Node) {
widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
widget.nodeListViewModel.nodes.add(res);
}
},
onTap: () {},
isSelected: true,
isPow: widget.isPow)),
),
content: Column(
children: [
if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
Observer(
builder: (_) => SettingsSwitcherCell(
key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'),
title: S.current.enable_auto_node_switching,
value: isPow
? powNodeListViewModel!.enableAutomaticNodeSwitching
: nodeListViewModel!.enableAutomaticNodeSwitching,
onValueChange: (BuildContext context, bool value) {
if (isPow) {
powNodeListViewModel!.setEnableAutomaticNodeSwitching(value);
} else {
nodeListViewModel!.setEnableAutomaticNodeSwitching(value);
}
},
),
),
if (FeatureFlag.isAutomaticNodeSwitchingEnabled) SizedBox(height: 8),
Observer(
builder: (BuildContext context) {
int itemsCount =
nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length;
return SectionStandardList(
scrollController: ModalScrollController.of(context),
sectionCount: 1,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
dividerPadding: EdgeInsets.symmetric(horizontal: 24),
itemCounter: (int sectionIndex) => itemsCount,
itemBuilder: (_, index) {
return Observer(
builder: (context) {
final node =
nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index];
late bool isSelected;
if (isPow) {
isSelected = node.id == powNodeListViewModel!.currentNode.id;
} else {
isSelected = node.id == nodeListViewModel!.currentNode.id;
}
final nodeListRow = NodeListRow(
title: node.uriRaw,
subtitle: node.label ?? '',
node: node,
isSelected: isSelected,
isPow: isPow,
onTap: (_) async {
if (isSelected) {
return;
}
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18)),
child: Observer(
builder: (BuildContext context) {
int itemsCount = widget.nodeListViewModel.nonCurrentNodes.length;
return ListView.separated(
controller: ModalScrollController.of(context),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemCount: itemsCount,
separatorBuilder: (context, index) => Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
),
itemBuilder: (_, index) {
return Observer(
builder: (context) {
final node = widget.nodeListViewModel.nonCurrentNodes[index];
final nodeListRow = NodeListRow(
node: node,
isSelected: false,
isPow: widget.isPow,
speed: widget.nodeListViewModel.nodeSpeedFor(node),
onEditComplete: (res) async {
if(res != null && res is Node) {
widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
widget.nodeListViewModel.nodes.add(res);
}
},
onTap: () async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).change_current_node_title,
alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ??
powNodeListViewModel!.getAlertContent(node.uriRaw),
alertContent:
widget.nodeListViewModel.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () async {
if (isPow) {
await powNodeListViewModel!.setAsCurrent(node);
} else {
await nodeListViewModel!.setAsCurrent(node);
}
await widget.nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
);
@@ -132,32 +141,16 @@ class ManageNodesPage extends BasePage {
);
},
);
return nodeListRow;
},
);
},
);
},
return nodeListRow;
},
);
},
);
},
),
),
],
),
);
}
Future<void> _presentReconnectAlert(BuildContext context) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).reconnection,
alertContent: S.of(context).reconnect_alert_text,
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
actionRightButton: () async {
Navigator.of(context).pop();
await dashboardViewModel!.reconnect();
},
actionLeftButton: () => Navigator.of(context).pop());
},
);
}
}
@@ -3,10 +3,7 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
@@ -20,7 +17,7 @@ class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase
abstract class NodeCreateOrEditViewModelBase with Store {
NodeCreateOrEditViewModelBase(
this.isPow, this.nodeListViewModel,this.powNodeListViewModel, this.walletType, this._settingsStore, {this.editingNode})
this.isPow, this.walletType, this._settingsStore, {this.editingNode})
: state = InitialExecutionState(),
connectionState = InitialExecutionState(),
label = editingNode?.label ?? '',
@@ -116,8 +113,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@observable
bool isPow;
final NodeListViewModel? nodeListViewModel;
final PowNodeListViewModel? powNodeListViewModel;
@computed
bool get isReady =>
@@ -257,12 +252,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@action
Future<void> delete({required Node editingNode}) async {
await editingNode.delete();
if(nodeListViewModel != null) {
nodeListViewModel!.bindNodes();
}
if(powNodeListViewModel != null) {
powNodeListViewModel!.bindNodes();
}
}
@action
@@ -287,12 +276,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
state = ExecutedSuccessfullyState();
if(nodeListViewModel != null) {
nodeListViewModel!.bindNodes();
}
if(powNodeListViewModel != null) {
powNodeListViewModel!.bindNodes();
}
} catch (e) {
state = FailureState(e.toString());
@@ -1,12 +1,14 @@
import 'dart:math';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/node_list.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/evm/evm.dart';
@@ -14,11 +16,34 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
part 'node_list_view_model.g.dart';
class NodeSpeed {
final String iconPath;
// fraction of best node ping needed to be classified as this speed.
final double treshold;
const NodeSpeed._(this.iconPath, this.treshold);
static const disconnected = NodeSpeed._(
"assets/new-ui/node_speed_badges/disconnected.svg", 0);
static const slow = NodeSpeed._(
"assets/new-ui/node_speed_badges/slow.svg", 0);
static const medium = NodeSpeed._(
"assets/new-ui/node_speed_badges/medium.svg", 0.5);
static const fast = NodeSpeed._(
"assets/new-ui/node_speed_badges/fast.svg", 0.8);
static const all = [fast, medium, slow, disconnected];
}
class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel;
abstract class NodeListViewModelBase with Store {
NodeListViewModelBase(this._appStore)
NodeListViewModelBase(this._appStore, this.isPow)
: nodes = ObservableList<Node>(),
_nodeSpeeds = ObservableMap<String, int>(),
settingsStore = _appStore.settingsStore {
bindNodes();
@@ -37,6 +62,7 @@ abstract class NodeListViewModelBase with Store {
bindNodes();
});
}
@computed
Node get currentNode {
@@ -52,7 +78,7 @@ abstract class NodeListViewModelBase with Store {
return settingsStore.getCurrentNode(walletType, chainId: chainId);
}
final node = settingsStore.nodes[walletType];
final node = isPow ? settingsStore.powNodes[walletType] : settingsStore.nodes[walletType];
if (node == null) {
throw Exception('No node for wallet type: $walletType');
}
@@ -73,6 +99,59 @@ abstract class NodeListViewModelBase with Store {
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final AppStore _appStore;
final bool isPow;
final ObservableMap<String, int> _nodeSpeeds;
@computed
List<Node> get nonCurrentNodes => nodes.where((item)=>item != currentNode).toList();
@observable
bool isTestingNodeSpeed = false;
Future<void> speedTestNodes() async {
try {
isTestingNodeSpeed = true;
_nodeSpeeds.clear();
final nodes = this.nodes.toList();
await Future.wait(nodes.map((node) async {
final sw = Stopwatch()..start();
final res = await node
.requestNode()
.timeout(const Duration(seconds: 10), onTimeout: () => false);
sw.stop();
if (res) {
_nodeSpeeds[node.uriRaw] = sw.elapsedMilliseconds;
}
}));
} finally {
isTestingNodeSpeed = false;
}
}
NodeSpeed? nodeSpeedFor(Node node) {
final bestSpeed = _bestNodeSpeed;
final currentSpeed = _nodeSpeeds[node.uriRaw];
if (bestSpeed == null) {
return null;
}
if (currentSpeed == null) {
return NodeSpeed.disconnected;
}
for (final speed in NodeSpeed.all) {
if (currentSpeed < (bestSpeed / speed.treshold)) return speed;
}
return null;
}
int? get _bestNodeSpeed {
try {
printV(_nodeSpeeds);
return _nodeSpeeds.values.reduce(min);
} catch (e) {}
return null;
}
Future<void> reset() async {
await resetToDefault();
@@ -125,7 +204,12 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
settingsStore.nodes[walletType] = node;
if(isPow) {
settingsStore.powNodes[walletType] = node;
} else {
settingsStore.nodes[walletType] = node;
}
}
@action
@@ -150,7 +234,7 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
nodes.addAll(await Node.getAllForWalletType(walletType));
nodes.addAll(isPow ? await Node.getAllForWalletTypePow(walletType) : await Node.getAllForWalletType(walletType));
}
}
@@ -1,83 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/mobx.dart';
import 'package:cw_core/node_list.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
part 'pow_node_list_view_model.g.dart';
class PowNodeListViewModel = PowNodeListViewModelBase with _$PowNodeListViewModel;
abstract class PowNodeListViewModelBase with Store {
PowNodeListViewModelBase(this._appStore)
: nodes = ObservableList<Node>(),
settingsStore = _appStore.settingsStore {
bindNodes();
reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
bindNodes();
});
}
@computed
Node get currentNode {
final node = settingsStore.powNodes[_appStore.wallet!.type];
if (node == null) {
throw Exception('No node for wallet type: ${_appStore.wallet!.type}');
}
return node;
}
String getAlertContent(String uri) =>
S.current.change_current_node(uri) +
'${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance!.enabled ? '' : S.current.orbot_running_alert) : ''}';
@computed
bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
@action
void setEnableAutomaticNodeSwitching(bool value) =>
settingsStore.enableAutomaticNodeSwitching = value;
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final AppStore _appStore;
Future<void> reset() async {
await resetPowToDefault();
Node node;
switch (_appStore.wallet!.type) {
case WalletType.nano:
node = (await getNanoDefaultPowNode())!;
break;
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
}
await setAsCurrent(node);
}
@action
Future<void> delete(Node node) async => node.delete();
Future<void> setAsCurrent(Node node) async =>
settingsStore.powNodes[_appStore.wallet!.type] = node;
@action
Future<void> bindNodes() async {
nodes.clear();
nodes.addAll(await Node.getAllPow());
}
}