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