diff --git a/assets/translations/ca.json b/assets/translations/ca.json index 41d93bd08e3860744f9c4b1026d55f60e1fc653e..19f22ee8d9bda5aff3863ddd9d1f577351d7acb9 100644 --- a/assets/translations/ca.json +++ b/assets/translations/ca.json @@ -59,8 +59,6 @@ "qr-scanner-title": "Escaneja el QR d'algú", "copy_contact_key": "Copia", "nothing_found": "No s'ha trobat res", - "using_nodes": "Usant {nodes} nodes de {type}", - "using_nodes_first": "El més rà pid: {node}", "technical_info_title": "Informació tècnica", "pattern_do_not_match": "Els patrons no coincideixen", "at_least_3": "Com a mÃnim calen 3 punts", @@ -120,7 +118,6 @@ "no_transactions": "Aquest moneder no té saldo. Comença per exemple a oferir els teus serveis als mercats Äž1 per rebre els teus primers ingressos.", "reloading_nodes": "Refrescant nodes del tipus {type}", "language_switch_title": "Tria la teva llengua", - "long_press_to_refresh": "Reordenant el llistat actual de nodes. Mantingues premut per refrescar la llista.", "error_installing_desktop": "Error en instal·lar Äž1nkgo al vostre escriptori (error: {error})", "install_desktop": "Instal·la Äž1nkgo a l'escriptori", "import_failed": "La importació del moneder ha fallat", diff --git a/assets/translations/de.json b/assets/translations/de.json index ebb8231e972374060d02034aa01145b5e039016f..f805554bb112d4618ee6d70c2bd3f828b90d3427 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -41,8 +41,6 @@ "add_contact": "Kontakt hinzufügen", "contact_added": "Kontakt hinzugefügt", "qr-scanner-title": "Scannen Sie den QR von jemanden", - "using_nodes": "Es werden {nodes} Knoten von {type} genutzt", - "using_nodes_first": "Schnellster: {node}", "at_least_3": "Mindestens 3 Punkte sind erforderlich", "intro_some_pattern_to_export": "Muster für den Import/Export Ihrer Brieftasche", "confirm_pattern": "Muster bestätigen", @@ -95,6 +93,5 @@ "intro_2_description": "Die Äž1 ist unabhängig von jeglichen Regierungen oder Gesellschaften, ist ökologisch (durch niedrigen Energieverbrauch), ist transparent und gerecht allen Beteiligten gegenüber.", "intro_3_description": "Duniter ist ein dezentralisiertes Kryptowährungsnetzwerk, das die Leute ermöglicht, ihre eigene Äž1 Währung zu schaffen.", "intro_4_description": "Wir helfen Ihnen, eine Äž1-Brieftasche zu eröffnen, damit Sie Ihre Äž1-Währung einfach und sicher auf Ihrem Gerät speichern und anderen ohne Zwischenwege überweisen können.", - "keys_tooltip": "Öffentliche und private Schlüssel sind in der Äž1 und in Duniter wie ein Schlüssel-Schloss-System. Dabei schließt der öffentliche Schlüssel ab, und nur der passende private Schlüssel kann öffnen. Das ist eine sichere Weise, Transaktionen zu authentifizieren und zu überprüfen.", - "long_press_to_refresh": "Neues Anordnen der gegenwärtigen Knoten-Liste. Tippen und halten, um die Liste zu aktualisieren." + "keys_tooltip": "Öffentliche und private Schlüssel sind in der Äž1 und in Duniter wie ein Schlüssel-Schloss-System. Dabei schließt der öffentliche Schlüssel ab, und nur der passende private Schlüssel kann öffnen. Das ist eine sichere Weise, Transaktionen zu authentifizieren und zu überprüfen." } diff --git a/assets/translations/en.json b/assets/translations/en.json index f6f5f72952858c940824aa3fe9280d9a69795de4..668f8291a181991f4eadfcf0086124f9d15e0117 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -60,9 +60,6 @@ "qr-scanner-title": "Scan the QR of someone", "copy_contact_key": "Copy", "nothing_found": "Nothing found", - "using_nodes": "Using {nodes} nodes of {type}", - "using_nodes_first": "Faster: {node}", - "long_press_to_refresh": "Reordering the current list of nodes. Tap and hold to refresh the list.", "technical_info_title": "Technical info", "pattern_do_not_match": "Patterns do not match", "at_least_3": "At least 3 points required", diff --git a/assets/translations/es.json b/assets/translations/es.json index dcddcebae7cbdc681ba83744de0a575f6571bff8..d36c499999016e932f08d5231e098e77cb624076 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -60,9 +60,6 @@ "qr-scanner-title": "Escanea el QR de alguien", "copy_contact_key": "Copiar", "nothing_found": "No se ha encontrado nada", - "using_nodes": "Usando {nodes} nodos de {type}", - "using_nodes_first": "Más rápido: {node}", - "long_press_to_refresh": "Reordenando la lista actual de nodos. Manten pulsado para refrescar la lista.", "technical_info_title": "Información técnica", "pattern_do_not_match": "Los patrones no coinciden", "at_least_3": "Se requieren al menos 3 puntos", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 826e06c77e193e3fdafc6a8b291e5401174a27fa..9bd9c5dc920935dcc60f8267424cd5bfe78db1b0 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -54,8 +54,6 @@ "no_contacts": "Vous n'avez pas encore de contacts", "data_load_error": "Erreur de chargement des données", "nothing_found": "Rien trouvé", - "using_nodes": "Utilisation de {nodes} nÅ“uds de type {type}", - "using_nodes_first": "Plus rapide : {node}", "technical_info_title": "Informations techniques", "pattern_do_not_match": "Les modèles ne correspondent pas", "at_least_3": "Au moins 3 points sont requis", @@ -64,7 +62,6 @@ "draw_pattern": "Dessiner le motif", "some_key_copied_to_clipboard": "La clé publique a été copiée dans le presse-papiers", "copy_contact_key": "Copie", - "long_press_to_refresh": "Réorganisation de la liste actuelle des nÅ“uds. Tapez sur un maintien pour actualiser la liste.", "cancel": "ANNULER", "import_config_desc": "Cela remplacera votre portefeuille actuel par le portefeuille importé", "transaction_received": "Reçu", diff --git a/assets/translations/gl.json b/assets/translations/gl.json index 8d06b827bf9f3533be62bc658c1568a7ca9f1e72..99b4fae863cc80e3a7892365cf51ac69e389c814 100644 --- a/assets/translations/gl.json +++ b/assets/translations/gl.json @@ -59,7 +59,6 @@ "contact_added": "Contacto engadido", "copy_contact_key": "Copiar", "nothing_found": "Non se atopou nada", - "using_nodes": "Usando {nodes} nodos de {type}", "skip": "Saltar", "wrong_pattern": "Patrón incorrecto", "cancel": "CANCELAR", @@ -89,8 +88,6 @@ "faq_1_title": "Podo importar a miña conta Cesium?", "faq_2_title": "É seguro utilizar este moedeiro?", "faq_3_title": "Por que non pódese agregar un contrasinal ao moedeiro?", - "using_nodes_first": "Máis rápido: {node}", - "long_press_to_refresh": "Reordenando a listaxe actual de nodos. Manter premado para refrescar a lista.", "technical_info_title": "Información técnica", "pattern_do_not_match": "Os patróns non coinciden", "at_least_3": "RequÃrense polo menos 3 puntos", diff --git a/assets/translations/nl.json b/assets/translations/nl.json index 629e8cdac8409fd14d8861cfbaed044c76640ac8..4452d8ddfaf121f81327be68c57d0bd7ff8ed11b 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -44,8 +44,6 @@ "add_contact": "Contact toevoegen", "contact_added": "Contact toegevoegd", "qr-scanner-title": "Scan de QR van iemand", - "using_nodes": "{nodes} nodes van {type} gebruiken", - "using_nodes_first": "Sneller: {node}", "bottom_nav_frd": "Saldo", "bottom_nav_fifth": "Info", "search_user": "Zoeken (gebruiker of openbare sleutel)", @@ -62,7 +60,6 @@ "no_transactions": "Deze portemonnee heeft geen saldo. Begin met het aanbieden van uw diensten in Äž1 markten, bijvoorbeeld, om uw eerste inkomen te ontvangen.", "copy_contact_key": "kopiëren", "nothing_found": "Niets gevonden", - "long_press_to_refresh": "De huidige lijst met knooppunten wordt opnieuw geordend. Tik en houd vast om de lijst te vernieuwen.", "technical_info_title": "Technische informatie", "pattern_do_not_match": "Patronen komen niet overeen", "at_least_3": "Minimaal 3 punten vereist", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index 0bb6e90b4255dcbd619a07a8fb1e8bf189c64428..f18ffc466d2fc04ee1ef87638ac54dd9d9a13681 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -77,8 +77,6 @@ "transactions": "Transacções", "nothing_found": "Nada encontrado", "wallet_exported": "Carteira exportada correctamente", - "using_nodes_first": "Rápido: {node}", - "using_nodes": "Usando {nodes} nós de {type}", "intro_1_description": "Com esta carteira, pode guardar, enviar e receber moeda Äž1 (também conhecida como 'Juna') de forma fácil e segura.", "intro_3_title": "A moeda Äž1 funciona na rede Duniter", "intro_3_description": "Duniter é uma rede descentralizada de criptomoeda que permite à s pessoas criar a sua própria moeda Äž1.", @@ -101,7 +99,6 @@ "no_transactions": "Esta carteira não tem saldo. Comece por oferecer os seus serviços nos mercados Äž1, por exemplo, para receber o seu primeiro rendimento.", "qr-scanner-title": "Digitalizar o QR de alguém", "copy_contact_key": "Copiar", - "long_press_to_refresh": "Reordenando a lista actual de nós. Tocar e segurar para actualizar a lista.", "technical_info_title": "Informação técnica", "pattern_do_not_match": "Padrões não coincidem", "at_least_3": "Pelo menos 3 pontos necessários", diff --git a/lib/data/models/node.g.dart b/lib/data/models/node.g.dart index 6b5dca27ea887629b8136bc2dcc87b68d74aaf4d..aa005531fd331ea25a0a396fb565545cf2f5678f 100644 --- a/lib/data/models/node.g.dart +++ b/lib/data/models/node.g.dart @@ -10,10 +10,12 @@ Node _$NodeFromJson(Map<String, dynamic> json) => Node( url: json['url'] as String, latency: json['latency'] as int? ?? 99999, errors: json['errors'] as int? ?? 0, + currentBlock: json['currentBlock'] as int? ?? 0, ); Map<String, dynamic> _$NodeToJson(Node instance) => <String, dynamic>{ 'url': instance.url, 'latency': instance.latency, 'errors': instance.errors, + 'currentBlock': instance.currentBlock, }; diff --git a/lib/data/models/node_list_cubit.dart b/lib/data/models/node_list_cubit.dart index c2434397b08cc2f67dd1806961d460cac6b76e11..96c8139973e0f7fee80cfbb83ca72f919ed3dd3a 100644 --- a/lib/data/models/node_list_cubit.dart +++ b/lib/data/models/node_list_cubit.dart @@ -10,6 +10,12 @@ class NodeListCubit extends HydratedCubit<NodeListState> { @override String get storagePrefix => 'NodeListCubit'; + bool get isLoading => state.isLoading; + + void setLoading(bool isLoading) { + emit(state.copyWith(isLoading: isLoading)); + } + void shuffle(NodeType type, bool withPenalty) { switch (type) { case NodeType.duniter: @@ -23,8 +29,8 @@ class NodeListCubit extends HydratedCubit<NodeListState> { case NodeType.cesiumPlus: if (withPenalty) { emit(state.copyWith( - cesiumPlusNodes: shuffleFirstNWithPenalty( - state.cesiumPlusNodes))); + cesiumPlusNodes: + shuffleFirstNWithPenalty(state.cesiumPlusNodes))); } else { emit(state.copyWith( cesiumPlusNodes: shuffleFirstN(state.cesiumPlusNodes))); diff --git a/lib/data/models/node_list_state.dart b/lib/data/models/node_list_state.dart index 2c7d480be357b29276b6a6ea7d2afe53efdd74cf..8a023c1ce1538ed9b68096d7be8c74ceefa7049f 100644 --- a/lib/data/models/node_list_state.dart +++ b/lib/data/models/node_list_state.dart @@ -14,10 +14,12 @@ class NodeListState extends Equatable { NodeListState( {List<Node>? duniterNodes, List<Node>? cesiumPlusNodes, - List<Node>? gvaNodes}) + List<Node>? gvaNodes, + bool? isLoading}) : duniterNodes = duniterNodes ?? defaultDuniterNodes, cesiumPlusNodes = cesiumPlusNodes ?? defaultCesiumPlusNodes, - gvaNodes = gvaNodes ?? defaultGvaNodes; + gvaNodes = gvaNodes ?? defaultGvaNodes, + isLoading = isLoading ?? false; factory NodeListState.fromJson(Map<String, dynamic> json) => _$NodeListStateFromJson(json); @@ -25,9 +27,11 @@ class NodeListState extends Equatable { final List<Node> duniterNodes; final List<Node> cesiumPlusNodes; final List<Node> gvaNodes; + final bool isLoading; @override - List<Object?> get props => <Object>[duniterNodes, cesiumPlusNodes, gvaNodes]; + List<Object?> get props => + <Object>[duniterNodes, cesiumPlusNodes, gvaNodes, isLoading]; Map<String, dynamic> toJson() => _$NodeListStateToJson(this); } diff --git a/lib/data/models/node_list_state.g.dart b/lib/data/models/node_list_state.g.dart index 0cc3c51959e0bac1ef0051c8b895bb540352747e..fe5c794fce98dd66831c326e2c6bf5f30ba1b464 100644 --- a/lib/data/models/node_list_state.g.dart +++ b/lib/data/models/node_list_state.g.dart @@ -13,6 +13,8 @@ abstract class _$NodeListStateCWProxy { NodeListState gvaNodes(List<Node>? gvaNodes); + NodeListState isLoading(bool? isLoading); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `NodeListState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -23,6 +25,7 @@ abstract class _$NodeListStateCWProxy { List<Node>? duniterNodes, List<Node>? cesiumPlusNodes, List<Node>? gvaNodes, + bool? isLoading, }); } @@ -43,6 +46,9 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { @override NodeListState gvaNodes(List<Node>? gvaNodes) => this(gvaNodes: gvaNodes); + @override + NodeListState isLoading(bool? isLoading) => this(isLoading: isLoading); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `NodeListState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -55,6 +61,7 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { Object? duniterNodes = const $CopyWithPlaceholder(), Object? cesiumPlusNodes = const $CopyWithPlaceholder(), Object? gvaNodes = const $CopyWithPlaceholder(), + Object? isLoading = const $CopyWithPlaceholder(), }) { return NodeListState( duniterNodes: duniterNodes == const $CopyWithPlaceholder() @@ -69,6 +76,10 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { ? _value.gvaNodes // ignore: cast_nullable_to_non_nullable : gvaNodes as List<Node>?, + isLoading: isLoading == const $CopyWithPlaceholder() + ? _value.isLoading + // ignore: cast_nullable_to_non_nullable + : isLoading as bool?, ); } } @@ -94,6 +105,7 @@ NodeListState _$NodeListStateFromJson(Map<String, dynamic> json) => gvaNodes: (json['gvaNodes'] as List<dynamic>?) ?.map((e) => Node.fromJson(e as Map<String, dynamic>)) .toList(), + isLoading: json['isLoading'] as bool?, ); Map<String, dynamic> _$NodeListStateToJson(NodeListState instance) => @@ -101,4 +113,5 @@ Map<String, dynamic> _$NodeListStateToJson(NodeListState instance) => 'duniterNodes': instance.duniterNodes, 'cesiumPlusNodes': instance.cesiumPlusNodes, 'gvaNodes': instance.gvaNodes, + 'isLoading': instance.isLoading, }; diff --git a/lib/data/models/node_manager.dart b/lib/data/models/node_manager.dart index ce1954e6f427eb00fd337e4cc65b9a3674e207b3..17f766a35a4097e5562b7e50ec8a4295561dcb17 100644 --- a/lib/data/models/node_manager.dart +++ b/lib/data/models/node_manager.dart @@ -104,6 +104,12 @@ class NodeManager { notifyObserver(); } + bool get loading => NodeManagerObserver.instance.cubit.isLoading; + + set loading(bool isLoading) { + NodeManagerObserver.instance.setLoading(isLoading); + } + void notifyObserver() { NodeManagerObserver.instance.update(this); } @@ -116,6 +122,10 @@ class NodeManagerObserver { late NodeListCubit cubit; + void setLoading(bool isLoading) { + cubit.setLoading(isLoading); + } + void update(NodeManager nodeManager) { cubit.setDuniterNodes(nodeManager.duniterNodes); cubit.setCesiumPlusNodes(nodeManager.cesiumPlusNodes); diff --git a/lib/g1/api.dart b/lib/g1/api.dart index 4241ffe3a04ffa686e0f9a8f951689a65e37cbc8..c4ec52b32d9230df3ce68fbd5d8fc256fafb5516 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -178,6 +178,7 @@ Future<Uint8List> getAvatar(String pubKey) async { Future<void> fetchDuniterNodes({bool force = false}) async { const NodeType type = NodeType.duniter; + NodeManager().loading = true; if (force || nodesWorking(type) < NodeManager.maxNodes) { if (force) { NodeManager().updateNodes(type, defaultDuniterNodes); @@ -193,11 +194,25 @@ Future<void> fetchDuniterNodes({bool force = false}) async { // developer.log(StackTrace.current.toString()); } } + NodeManager().loading = false; +} + +Future<void> fetchNodes(NodeType type) async { + if (type == NodeType.duniter) { + fetchDuniterNodes(force: true); + } else { + if (type == NodeType.cesiumPlus) { + fetchCesiumPlusNodes(force: true); + } else { + fetchGvaNodes(force: true); + } + } } // https://github.com/duniter/cesium/blob/467ec68114be650cd1b306754c3142fc4020164c/www/js/config.js#L96 // https://g1.data.le-sou.org/g1/peer/_search?pretty Future<void> fetchCesiumPlusNodes({bool force = false}) async { + NodeManager().loading = true; const NodeType type = NodeType.cesiumPlus; if (force) { NodeManager().updateNodes(type, defaultCesiumPlusNodes); @@ -207,9 +222,11 @@ Future<void> fetchCesiumPlusNodes({bool force = false}) async { } final List<Node> nodes = await _fetchNodes(NodeType.cesiumPlus); NodeManager().updateNodes(type, nodes); + NodeManager().loading = false; } Future<void> fetchGvaNodes({bool force = false}) async { + NodeManager().loading = true; const NodeType type = NodeType.gva; if (force) { NodeManager().updateNodes(type, defaultGvaNodes); @@ -219,6 +236,7 @@ Future<void> fetchGvaNodes({bool force = false}) async { } final List<Node> nodes = await _fetchDuniterNodesFromPeers(type); NodeManager().updateNodes(type, nodes); + NodeManager().loading = false; } int nodesWorking(NodeType type) => NodeManager() @@ -361,34 +379,49 @@ Future<List<Node>> _fetchNodes(NodeType type) async { } Future<NodeCheck> _pingNode(String node, NodeType type) async { + // Decrease timout during ping const Duration timeout = Duration(seconds: 10); int currentBlock = 0; + Duration latency; try { final Stopwatch stopwatch = Stopwatch()..start(); - if (type == NodeType.duniter || type == NodeType.cesiumPlus) { + if (type == NodeType.duniter) { + final Response response = await http + .get(Uri.parse('$node/blockchain/current')) + .timeout(timeout); + stopwatch.stop(); + latency = stopwatch.elapsed; + if (response.statusCode == 200) { + final Map<String, dynamic> json = + jsonDecode(response.body) as Map<String, dynamic>; + currentBlock = json['number'] as int; + } else { + latency = wrongNodeDuration; + } + } else if (type == NodeType.cesiumPlus) { + // see: http://g1.data.e-is.pro/network/peering await http - .get(Uri.parse(type == NodeType.duniter - ? '$node/network/peers/self/ping' - : type == NodeType.cesiumPlus - ? - // see: http://g1.data.e-is.pro/network/peering - '$node/network/peering' - // gva, test for playground - : '$node/playground')) + .get(Uri.parse('$node/network/peering')) // Decrease http timeout during ping .timeout(timeout); + stopwatch.stop(); + latency = stopwatch.elapsed; } else { // Test GVA with a query final Gva gva = Gva(node: proxyfyNode(node)); currentBlock = await gva.getCurrentBlock().timeout(timeout); // NodeManager().updateNode(type, node.copyWith(latency: newLatency)); + stopwatch.stop(); + final double balance = await gva + .balance('EdWkzNABz7dPancFqW6JVLqv1wpGaQSxgWmMf1pmY7KG') + .timeout(timeout); + latency = balance >= 0 ? stopwatch.elapsed : wrongNodeDuration; } - stopwatch.stop(); - return NodeCheck(latency: stopwatch.elapsed, currentBlock: currentBlock); + return NodeCheck(latency: latency, currentBlock: currentBlock); } catch (e) { // Handle exception when node is unavailable etc logger('Node $node does not respond to ping $e'); - return NodeCheck(latency: const Duration(days: 2), currentBlock: 0); + return NodeCheck(latency: wrongNodeDuration, currentBlock: 0); } } @@ -595,6 +628,7 @@ List<Node> _getBestGvaNodes() { // Fallback nodes.addAll(defaultGvaNodes); } + nodes.shuffle(); return nodes; } diff --git a/lib/g1/g1_helper.dart b/lib/g1/g1_helper.dart index 2f18f2f65814eb6fa6174b7699bc17cf57a827ca..91ac1d144f94e0ce93d44d8bcc471c9a5e3ffa97 100644 --- a/lib/g1/g1_helper.dart +++ b/lib/g1/g1_helper.dart @@ -163,3 +163,5 @@ Map<String, dynamic> decryptJsonForImport( .decrypt64(keyEncrypted, iv: _iv); return jsonDecode(decrypted) as Map<String, dynamic>; } + +const Duration wrongNodeDuration = Duration(days: 2); diff --git a/lib/ui/screens/debug_nodes_screen.dart b/lib/ui/screens/debug_nodes_screen.dart index 1015226ce4e827e374015ab4aa9da7946f67828d..de02e3ad832a33e5f2186615682afd798c73f640 100644 --- a/lib/ui/screens/debug_nodes_screen.dart +++ b/lib/ui/screens/debug_nodes_screen.dart @@ -6,6 +6,8 @@ import '../../data/models/node.dart'; import '../../data/models/node_list_cubit.dart'; import '../../data/models/node_list_state.dart'; import '../../data/models/node_type.dart'; +import '../../g1/api.dart'; +import '../ui_helpers.dart'; import '../widgets/debug_nodes/debug_node_list.dart'; class DebugNodesScreen extends StatelessWidget { @@ -19,55 +21,78 @@ class DebugNodesScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder<NodeListCubit, NodeListState>( - builder: (BuildContext nodeContext, NodeListState state) { - final List<Node> duniterNodes = - filterAndSortNodesByType(state.duniterNodes, NodeType.duniter); - final List<Node> cesiumPlusNodes = - filterAndSortNodesByType(state.cesiumPlusNodes, NodeType.cesiumPlus); - final List<Node> gvaNodes = - filterAndSortNodesByType(state.gvaNodes, NodeType.gva); - return Scaffold( - appBar: AppBar(title: Text(tr('nodes_tech_info'))), - body: Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.9), - child: Scrollbar( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - const Padding( - padding: EdgeInsets.all(8.0), - child: - Text('GVA Nodes', style: TextStyle(fontSize: 24)), - ), - DebugNodeList( - nodes: gvaNodes, - type: NodeType.gva, - currentBlock: gvaNodes[0].currentBlock), - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Duniter Nodes', - style: TextStyle(fontSize: 24)), - ), - DebugNodeList( - nodes: duniterNodes, - type: NodeType.duniter, - currentBlock: duniterNodes[0].currentBlock), - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Cesium+ Nodes', - style: TextStyle(fontSize: 24)), - ), - DebugNodeList( - nodes: cesiumPlusNodes, - type: NodeType.cesiumPlus, - currentBlock: cesiumPlusNodes[0].currentBlock), - ], - ), + final NodeListState state = context.watch<NodeListCubit>().state; + final List<Node> duniterNodes = + filterAndSortNodesByType(state.duniterNodes, NodeType.duniter); + final List<Node> cesiumPlusNodes = + filterAndSortNodesByType(state.cesiumPlusNodes, NodeType.cesiumPlus); + final List<Node> gvaNodes = + filterAndSortNodesByType(state.gvaNodes, NodeType.gva); + return Scaffold( + appBar: AppBar( + title: Text(tr('nodes_tech_info')), + bottom: state.isLoading + ? const PreferredSize( + preferredSize: Size.fromHeight(4.0), + child: LinearProgressIndicator(), + ) + : null, + ), + body: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.9), + child: Scrollbar( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + const DebugNodeHeader(type: NodeType.gva), + DebugNodeList( + nodes: gvaNodes, + type: NodeType.gva, + currentBlock: gvaNodes[0].currentBlock), + const DebugNodeHeader(type: NodeType.duniter), + DebugNodeList( + nodes: duniterNodes, + type: NodeType.duniter, + currentBlock: duniterNodes[0].currentBlock), + const DebugNodeHeader(type: NodeType.cesiumPlus), + DebugNodeList( + nodes: cesiumPlusNodes, + type: NodeType.cesiumPlus, + currentBlock: cesiumPlusNodes[0].currentBlock), + ], ), - ))); - }); + ), + ))); + } +} + +class DebugNodeHeader extends StatelessWidget { + const DebugNodeHeader({super.key, required this.type}); + + final NodeType type; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Text('${capitalize(type.name)} Nodes', + style: const TextStyle(fontSize: 20)), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + fetchNodes(type); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(tr('reloading_nodes', + namedArgs: <String, String>{'type': type.name})), + )); + }, + ), + ], + )); } } diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart index f6799780c17b80ce85cda5307f17d14810218a74..72ca630d37da6c6c0bd4cf2203b1c703e7689e2c 100644 --- a/lib/ui/screens/fifth_screen.dart +++ b/lib/ui/screens/fifth_screen.dart @@ -7,7 +7,6 @@ import 'package:share_plus/share_plus.dart'; import '../../cubit/theme_cubit.dart'; import '../../data/models/app_cubit.dart'; import '../../data/models/app_state.dart'; -import '../../data/models/node_type.dart'; import '../../shared_prefs.dart'; import '../notification_controller.dart'; import '../ui_helpers.dart'; @@ -20,7 +19,6 @@ import '../widgets/fifth_screen/import_dialog.dart'; import '../widgets/fifth_screen/link_card.dart'; import '../widgets/fifth_screen/node_info.dart'; import '../widgets/fifth_screen/text_divider.dart'; -import 'debug_nodes_screen.dart'; class FifthScreen extends StatelessWidget { const FifthScreen({super.key}); @@ -170,26 +168,7 @@ class FifthScreen extends StatelessWidget { ]), if (state.expertMode) const TextDivider(text: 'technical_info_title'), - if (state.expertMode) - const NodeInfoCard(type: NodeType.duniter), - if (state.expertMode) - const NodeInfoCard(type: NodeType.cesiumPlus), - if (state.expertMode) - const NodeInfoCard(type: NodeType.gva), - if (state - .expertMode) // context.read<AppState>().expertMode) - ListTile( - leading: const Icon(Icons.hub), - title: Text(tr('nodes_tech_info')), - onTap: () { - Navigator.push( - context, - MaterialPageRoute<VoidCallback>( - builder: (BuildContext context) => - const DebugNodesScreen()), - ); - }, - ), + if (state.expertMode) const NodeInfoCard(), if (state.expertMode) const TextDivider(text: 'info_links'), if (state.expertMode) LinkCard( diff --git a/lib/ui/widgets/debug_nodes/debug_node_list.dart b/lib/ui/widgets/debug_nodes/debug_node_list.dart index 966caa83be329ed62df5a90a774c7bd89fa12d7f..73b72e0c2f3d1942744ccda47b2eb12462e4d631 100644 --- a/lib/ui/widgets/debug_nodes/debug_node_list.dart +++ b/lib/ui/widgets/debug_nodes/debug_node_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../data/models/node.dart'; import '../../../data/models/node_type.dart'; +import '../../../g1/g1_helper.dart'; class DebugNodeList extends StatelessWidget { const DebugNodeList( @@ -16,22 +17,31 @@ class DebugNodeList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - //physics: const AlwaysScrollableScrollPhysics(), - itemCount: nodes.length, - itemBuilder: (BuildContext context, int index) { + return Column( + children: List<Widget>.generate( + nodes.length, + (int index) { final Node node = nodes[index]; - return ListTile( - dense: true, - title: Text(node.url), - subtitle: Text( - '${type != NodeType.cesiumPlus ? 'Current block: ${node.currentBlock}, ' : ''}errors: ${node.errors}, latency (ms): ${node.latency}'), - leading: node.currentBlock == currentBlock - ? const Icon(Icons.check_circle, color: Colors.green) - : const Icon(Icons.run_circle, color: Colors.grey), - ); + final int wrongNode = wrongNodeDuration.inMicroseconds; + return Theme( + data: Theme.of(context).copyWith( + visualDensity: VisualDensity.compact, + ), + child: ListTile( + dense: true, + title: Text(node.url), + subtitle: node.latency < wrongNode + ? Text( + '${type != NodeType.cesiumPlus ? 'Current block: ${node.currentBlock}, ' : ''}errors: ${node.errors}, latency (ms): ${node.latency}') + : null, + leading: + node.currentBlock == currentBlock && node.latency < wrongNode + ? const Icon(Icons.check_circle, color: Colors.green) + : node.latency < wrongNode + ? const Icon(Icons.run_circle, color: Colors.grey) + : const Icon(Icons.power_off, color: Colors.grey), + )); }, - ); + )); } } diff --git a/lib/ui/widgets/fifth_screen/node_info.dart b/lib/ui/widgets/fifth_screen/node_info.dart index 0861e9ff8af03deabf46dcbf5db1ded0251b71b6..9db9f186bb934b0ed324e238350f364595bd0b7b 100644 --- a/lib/ui/widgets/fifth_screen/node_info.dart +++ b/lib/ui/widgets/fifth_screen/node_info.dart @@ -2,68 +2,28 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../data/models/node.dart'; import '../../../data/models/node_list_cubit.dart'; import '../../../data/models/node_list_state.dart'; -import '../../../data/models/node_type.dart'; -import '../../../g1/api.dart'; -import '../../logger.dart'; +import '../../screens/debug_nodes_screen.dart'; import '../fifth_screen/info_card.dart'; class NodeInfoCard extends StatelessWidget { - const NodeInfoCard({super.key, required this.type}); - - final NodeType type; + const NodeInfoCard({super.key}); @override Widget build(BuildContext context) { return BlocBuilder<NodeListCubit, NodeListState>( builder: (BuildContext nodeContext, NodeListState state) { - final List<Node> nodes = type == NodeType.duniter - ? state.duniterNodes - : type == NodeType.cesiumPlus - ? state.cesiumPlusNodes - : state.gvaNodes; return GestureDetector( onTap: () { - context.read<NodeListCubit>().shuffle(type, true); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('long_press_to_refresh')), - ), + Navigator.push( + context, + MaterialPageRoute<VoidCallback>( + builder: (BuildContext context) => const DebugNodesScreen()), ); }, - onLongPress: () { - logger('On long press'); - if (type == NodeType.duniter) { - fetchDuniterNodes(force: true); - } else { - if (type == NodeType.cesiumPlus) { - fetchCesiumPlusNodes(force: true); - } else { - fetchGvaNodes(); - } - } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(tr('reloading_nodes', - namedArgs: <String, String>{'type': type.name})), - )); - }, child: InfoCard( - title: tr('using_nodes', namedArgs: <String, String>{ - 'type': type.name, - 'nodes': nodes.length.toString() - }), - translate: false, - subtitle: nodes.isNotEmpty - ? tr('using_nodes_first', - namedArgs: <String, String>{'node': nodes.first.url}) - : '', - icon: type == NodeType.duniter - ? Icons.hub - : type == NodeType.cesiumPlus - ? Icons.diversity_2 - : Icons.g_mobiledata)); + title: tr('nodes_tech_info'), translate: false, icon: Icons.hub)); }); } } diff --git a/pubspec.yaml b/pubspec.yaml index 306b8a3434ce272eacbf58fd90e9501aaea9ceed..9858c10edf745360c0db0df05de92b0b83962cb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.0.19 +version: 0.0.20-SNAPSHOT environment: sdk: ">=2.17.1 <3.0.0"