From e167ce30b7603f92d18f538a5536d8c5cad745c9 Mon Sep 17 00:00:00 2001
From: vjrj <vjrj@comunes.org>
Date: Sat, 30 Dec 2023 15:26:20 +0100
Subject: [PATCH] Refactor node list widgets

---
 lib/ui/screens/fifth_screen.dart              |   4 +-
 lib/ui/screens/node_list_page.dart            | 114 ++++++++++++++++++
 .../widgets/fifth_screen/node_list_card.dart  |  29 +++++
 .../widgets/node_list/node_list_widget.dart   |  53 ++++++++
 4 files changed, 198 insertions(+), 2 deletions(-)
 create mode 100644 lib/ui/screens/node_list_page.dart
 create mode 100644 lib/ui/widgets/fifth_screen/node_list_card.dart
 create mode 100644 lib/ui/widgets/node_list/node_list_widget.dart

diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart
index e6e40588..23d60cc7 100644
--- a/lib/ui/screens/fifth_screen.dart
+++ b/lib/ui/screens/fifth_screen.dart
@@ -21,7 +21,7 @@ import '../widgets/fifth_screen/fifth_tutorial.dart';
 import '../widgets/fifth_screen/grid_item.dart';
 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/node_list_card.dart';
 import '../widgets/fifth_screen/text_divider.dart';
 
 class FifthScreen extends StatefulWidget {
@@ -216,7 +216,7 @@ class _FifthScreenState extends State<FifthScreen> {
                             }),
                       if (state.expertMode)
                         const TextDivider(text: 'technical_info_title'),
-                      if (state.expertMode) const NodeInfoCard(),
+                      if (state.expertMode) const NodeListCard(),
                       const TextDivider(text: 'info_links'),
                       if (state.expertMode)
                         LinkCard(
diff --git a/lib/ui/screens/node_list_page.dart b/lib/ui/screens/node_list_page.dart
new file mode 100644
index 00000000..0a7a2d63
--- /dev/null
+++ b/lib/ui/screens/node_list_page.dart
@@ -0,0 +1,114 @@
+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 '../../g1/no_nodes_exception.dart';
+import '../ui_helpers.dart';
+import '../widgets/node_list/node_list_widget.dart';
+
+class NodeListPage extends StatelessWidget {
+  const NodeListPage({super.key});
+
+  List<Node> filterAndSortNodesByType(List<Node> nodes, NodeType type) {
+    nodes.sort(
+        (Node a, Node b) => a.currentBlock.compareTo(b.currentBlock) * -1);
+    return nodes;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    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),
+                    if (gvaNodes.isNotEmpty)
+                      NodeListWidget(
+                          nodes: gvaNodes,
+                          type: NodeType.gva,
+                          currentBlock: gvaNodes[0].currentBlock),
+                    const DebugNodeHeader(type: NodeType.duniter),
+                    if (duniterNodes.isNotEmpty)
+                      NodeListWidget(
+                          nodes: duniterNodes,
+                          type: NodeType.duniter,
+                          currentBlock: duniterNodes[0].currentBlock),
+                    const DebugNodeHeader(type: NodeType.cesiumPlus),
+                    if (cesiumPlusNodes.isNotEmpty)
+                      NodeListWidget(
+                          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)),
+            GestureDetector(
+              onLongPress: () => _fetchNodes(context, true),
+              child: IconButton(
+                  icon: const Icon(Icons.refresh),
+                  // Force in all cases
+                  onPressed: () => _fetchNodes(context, true)),
+            )
+          ],
+        ));
+  }
+
+  void _fetchNodes(BuildContext context, bool force) {
+    try {
+      fetchNodes(type, force);
+      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+        content: Text(tr('reloading_nodes',
+            namedArgs: <String, String>{'type': type.name})),
+      ));
+    } on NoNodesException {
+      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+        content: Text(tr('no_nodes_found',
+            namedArgs: <String, String>{'type': type.name})),
+      ));
+    }
+  }
+}
diff --git a/lib/ui/widgets/fifth_screen/node_list_card.dart b/lib/ui/widgets/fifth_screen/node_list_card.dart
new file mode 100644
index 00000000..5fff2ac4
--- /dev/null
+++ b/lib/ui/widgets/fifth_screen/node_list_card.dart
@@ -0,0 +1,29 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../../data/models/node_list_cubit.dart';
+import '../../../data/models/node_list_state.dart';
+import '../../screens/node_list_page.dart';
+import '../fifth_screen/info_card.dart';
+
+class NodeListCard extends StatelessWidget {
+  const NodeListCard({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<NodeListCubit, NodeListState>(
+        builder: (BuildContext nodeContext, NodeListState state) {
+      return GestureDetector(
+          onTap: () {
+            Navigator.push(
+              context,
+              MaterialPageRoute<VoidCallback>(
+                  builder: (BuildContext context) => const NodeListPage()),
+            );
+          },
+          child: InfoCard(
+              title: tr('nodes_tech_info'), translate: false, icon: Icons.hub));
+    });
+  }
+}
diff --git a/lib/ui/widgets/node_list/node_list_widget.dart b/lib/ui/widgets/node_list/node_list_widget.dart
new file mode 100644
index 00000000..d6f0741e
--- /dev/null
+++ b/lib/ui/widgets/node_list/node_list_widget.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+
+import '../../../data/models/node.dart';
+import '../../../data/models/node_type.dart';
+import '../../../g1/g1_helper.dart';
+import '../../ui_helpers.dart';
+
+class NodeListWidget extends StatelessWidget {
+  const NodeListWidget(
+      {super.key,
+      required this.nodes,
+      required this.currentBlock,
+      required this.type});
+
+  final List<Node> nodes;
+  final int currentBlock;
+  final NodeType type;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+        children: List<Widget>.generate(
+      nodes.length,
+      (int index) {
+        final Node node = nodes[index];
+        final int wrongNode = wrongNodeDuration.inMicroseconds;
+        return Theme(
+            data: Theme.of(context).copyWith(
+              visualDensity: VisualDensity.compact,
+            ),
+            child: GestureDetector(
+                onTap: () => copyToClipboard(
+                    context: context,
+                    uri: node.url,
+                    feedbackText: 'copied_to_clipboard'),
+                child: ListTile(
+                  dense: true,
+                  title: Text(node.url),
+                  subtitle: node.latency < wrongNode
+                      ? Text(
+                          '${type != NodeType.cesiumPlus ? 'Current block: ${node.currentBlock}, ' : 'Current docs: ${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),
+                )));
+      },
+    ));
+  }
+}
-- 
GitLab