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,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());
}
}