diff --git a/assets/translations/en.json b/assets/translations/en.json index 85dad465798713daf474a9e8a071e7896b963791..287446b87624845bfe830f27ed60660b1f851795 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -376,6 +376,8 @@ "create_with_mnemonics_option": "With mnemonic phrase", "create_with_mnemonics_description": "Recoverable wallet using a seed phrase (recommended in duniter v2)", "clipboard_import_mnemonic": "Import mnemonic phrase", - "clipboard_import_mnemonic_description": "Enter a mnemonic phrase to recover your wallet" + "clipboard_import_mnemonic_description": "Enter a mnemonic phrase to recover your wallet", + "nodes_list_title": "{} Nodes ({})", + "nodes_list_last_updated": "Last updated: {}" } diff --git a/assets/translations/es.json b/assets/translations/es.json index 269c835c8ede2205ad952f20f58726ad521efc6d..affaf9348ce701d1b8ada8fb7debe179643c5554 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -382,5 +382,7 @@ "create_with_mnemonics_option": "Con frase mnemónica", "create_with_mnemonics_description": "Monedero recuperable utilizando una frase semilla (recomendado en Duniter v2)", "clipboard_import_mnemonic": "Importar frase mnemónica", - "clipboard_import_mnemonic_description": "Introduce una frase mnemónica para recuperar tu monedero" + "clipboard_import_mnemonic_description": "Introduce una frase mnemónica para recuperar tu monedero", + "nodes_list_title": "Nodos {} ({})", + "nodes_list_last_updated": "Última actualización: {}" } diff --git a/lib/data/models/node_list_cubit.dart b/lib/data/models/node_list_cubit.dart index 643acd9fc59c90238294e0b3428080c25659ce75..4b0f71b4002fca3bf189848db35363afc5ba2e51 100644 --- a/lib/data/models/node_list_cubit.dart +++ b/lib/data/models/node_list_cubit.dart @@ -33,31 +33,38 @@ class NodeListCubit extends HydratedCubit<NodeListState> { } void setDuniterNodes(List<Node> nodes) { - emit(state.copyWith(duniterNodes: nodes)); + emit(state.copyWith( + duniterNodes: nodes, duniterNodesLastUpdate: DateTime.now())); } void setDuniterIndexerNodes(List<Node> nodes) { - emit(state.copyWith(duniterIndexerNodes: nodes)); + emit(state.copyWith( + duniterIndexerNodes: nodes, + duniterIndexerNodesLastUpdate: DateTime.now())); } void setDuniterDataNodes(List<Node> nodes) { - emit(state.copyWith(duniterDataNodes: nodes)); + emit(state.copyWith( + duniterDataNodes: nodes, duniterDataNodesLastUpdate: DateTime.now())); } void setIpfsGateways(List<Node> nodes) { - emit(state.copyWith(ipfsGateways: nodes)); + emit(state.copyWith( + ipfsGateways: nodes, ipfsGatewaysLastUpdate: DateTime.now())); } void setCesiumPlusNodes(List<Node> nodes) { - emit(state.copyWith(cesiumPlusNodes: nodes)); + emit(state.copyWith( + cesiumPlusNodes: nodes, cesiumPlusNodesLastUpdate: DateTime.now())); } void setGvaNodes(List<Node> nodes) { - emit(state.copyWith(gvaNodes: nodes)); + emit(state.copyWith(gvaNodes: nodes, gvaNodesLastUpdate: DateTime.now())); } void setEndpointNodes(List<Node> nodes) { - emit(state.copyWith(endpointNodes: nodes)); + emit(state.copyWith( + endpointNodes: nodes, endpointNodesLastUpdate: DateTime.now())); } List<Node> get duniterNodes => state.duniterNodes; diff --git a/lib/data/models/node_list_state.dart b/lib/data/models/node_list_state.dart index 8e7af7b79e512b8e1cbd81b6c1134054af2c703e..ef52cd7058c359d2256434c6067ab5fb31487fca 100644 --- a/lib/data/models/node_list_state.dart +++ b/lib/data/models/node_list_state.dart @@ -12,17 +12,24 @@ part 'node_list_state.g.dart'; @JsonSerializable() @CopyWith() class NodeListState extends Equatable { - NodeListState( - {List<Node>? duniterNodes, - List<Node>? cesiumPlusNodes, - List<Node>? gvaNodes, - List<Node>? endpointNodes, - List<Node>? duniterIndexerNodes, - List<Node>? duniterDataNodes, - List<Node>? ipfsGateways, - this.currentGvaNode, - bool? isLoading}) - : duniterNodes = duniterNodes ?? defaultDuniterNodes, + NodeListState({ + List<Node>? duniterNodes, + List<Node>? cesiumPlusNodes, + List<Node>? gvaNodes, + List<Node>? endpointNodes, + List<Node>? duniterIndexerNodes, + List<Node>? duniterDataNodes, + List<Node>? ipfsGateways, + this.currentGvaNode, + this.duniterNodesLastUpdate, + this.cesiumPlusNodesLastUpdate, + this.gvaNodesLastUpdate, + this.endpointNodesLastUpdate, + this.duniterIndexerNodesLastUpdate, + this.duniterDataNodesLastUpdate, + this.ipfsGatewaysLastUpdate, + bool? isLoading, + }) : duniterNodes = duniterNodes ?? defaultDuniterNodes, cesiumPlusNodes = cesiumPlusNodes ?? defaultCesiumPlusNodes, gvaNodes = gvaNodes ?? defaultGvaNodes, endpointNodes = endpointNodes ?? defaultEndPointNodes, @@ -48,6 +55,15 @@ class NodeListState extends Equatable { final List<Node> duniterDataNodes; @JsonKey(fromJson: _nodesFromJson, toJson: _nodesToJson) final List<Node> ipfsGateways; + + final DateTime? duniterNodesLastUpdate; + final DateTime? cesiumPlusNodesLastUpdate; + final DateTime? gvaNodesLastUpdate; + final DateTime? endpointNodesLastUpdate; + final DateTime? duniterIndexerNodesLastUpdate; + final DateTime? duniterDataNodesLastUpdate; + final DateTime? ipfsGatewaysLastUpdate; + final bool isLoading; @JsonKey(fromJson: _nodeFromJson, toJson: _nodeToJson) final Node? currentGvaNode; @@ -62,6 +78,13 @@ class NodeListState extends Equatable { duniterDataNodes, ipfsGateways, currentGvaNode, + duniterNodesLastUpdate, + cesiumPlusNodesLastUpdate, + gvaNodesLastUpdate, + endpointNodesLastUpdate, + duniterIndexerNodesLastUpdate, + duniterDataNodesLastUpdate, + ipfsGatewaysLastUpdate, isLoading ]; diff --git a/lib/data/models/node_list_state.g.dart b/lib/data/models/node_list_state.g.dart index a697bfd3dadd152f3ea84316e53f0c3b783671d5..2338cf0a6d02846a915ce27bbec215880c050c11 100644 --- a/lib/data/models/node_list_state.g.dart +++ b/lib/data/models/node_list_state.g.dart @@ -23,6 +23,22 @@ abstract class _$NodeListStateCWProxy { NodeListState currentGvaNode(Node? currentGvaNode); + NodeListState duniterNodesLastUpdate(DateTime? duniterNodesLastUpdate); + + NodeListState cesiumPlusNodesLastUpdate(DateTime? cesiumPlusNodesLastUpdate); + + NodeListState gvaNodesLastUpdate(DateTime? gvaNodesLastUpdate); + + NodeListState endpointNodesLastUpdate(DateTime? endpointNodesLastUpdate); + + NodeListState duniterIndexerNodesLastUpdate( + DateTime? duniterIndexerNodesLastUpdate); + + NodeListState duniterDataNodesLastUpdate( + DateTime? duniterDataNodesLastUpdate); + + NodeListState ipfsGatewaysLastUpdate(DateTime? ipfsGatewaysLastUpdate); + 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. @@ -40,6 +56,13 @@ abstract class _$NodeListStateCWProxy { List<Node>? duniterDataNodes, List<Node>? ipfsGateways, Node? currentGvaNode, + DateTime? duniterNodesLastUpdate, + DateTime? cesiumPlusNodesLastUpdate, + DateTime? gvaNodesLastUpdate, + DateTime? endpointNodesLastUpdate, + DateTime? duniterIndexerNodesLastUpdate, + DateTime? duniterDataNodesLastUpdate, + DateTime? ipfsGatewaysLastUpdate, bool? isLoading, }); } @@ -81,6 +104,37 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { NodeListState currentGvaNode(Node? currentGvaNode) => this(currentGvaNode: currentGvaNode); + @override + NodeListState duniterNodesLastUpdate(DateTime? duniterNodesLastUpdate) => + this(duniterNodesLastUpdate: duniterNodesLastUpdate); + + @override + NodeListState cesiumPlusNodesLastUpdate( + DateTime? cesiumPlusNodesLastUpdate) => + this(cesiumPlusNodesLastUpdate: cesiumPlusNodesLastUpdate); + + @override + NodeListState gvaNodesLastUpdate(DateTime? gvaNodesLastUpdate) => + this(gvaNodesLastUpdate: gvaNodesLastUpdate); + + @override + NodeListState endpointNodesLastUpdate(DateTime? endpointNodesLastUpdate) => + this(endpointNodesLastUpdate: endpointNodesLastUpdate); + + @override + NodeListState duniterIndexerNodesLastUpdate( + DateTime? duniterIndexerNodesLastUpdate) => + this(duniterIndexerNodesLastUpdate: duniterIndexerNodesLastUpdate); + + @override + NodeListState duniterDataNodesLastUpdate( + DateTime? duniterDataNodesLastUpdate) => + this(duniterDataNodesLastUpdate: duniterDataNodesLastUpdate); + + @override + NodeListState ipfsGatewaysLastUpdate(DateTime? ipfsGatewaysLastUpdate) => + this(ipfsGatewaysLastUpdate: ipfsGatewaysLastUpdate); + @override NodeListState isLoading(bool? isLoading) => this(isLoading: isLoading); @@ -101,6 +155,13 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { Object? duniterDataNodes = const $CopyWithPlaceholder(), Object? ipfsGateways = const $CopyWithPlaceholder(), Object? currentGvaNode = const $CopyWithPlaceholder(), + Object? duniterNodesLastUpdate = const $CopyWithPlaceholder(), + Object? cesiumPlusNodesLastUpdate = const $CopyWithPlaceholder(), + Object? gvaNodesLastUpdate = const $CopyWithPlaceholder(), + Object? endpointNodesLastUpdate = const $CopyWithPlaceholder(), + Object? duniterIndexerNodesLastUpdate = const $CopyWithPlaceholder(), + Object? duniterDataNodesLastUpdate = const $CopyWithPlaceholder(), + Object? ipfsGatewaysLastUpdate = const $CopyWithPlaceholder(), Object? isLoading = const $CopyWithPlaceholder(), }) { return NodeListState( @@ -136,6 +197,40 @@ class _$NodeListStateCWProxyImpl implements _$NodeListStateCWProxy { ? _value.currentGvaNode // ignore: cast_nullable_to_non_nullable : currentGvaNode as Node?, + duniterNodesLastUpdate: + duniterNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.duniterNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : duniterNodesLastUpdate as DateTime?, + cesiumPlusNodesLastUpdate: + cesiumPlusNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.cesiumPlusNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : cesiumPlusNodesLastUpdate as DateTime?, + gvaNodesLastUpdate: gvaNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.gvaNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : gvaNodesLastUpdate as DateTime?, + endpointNodesLastUpdate: + endpointNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.endpointNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : endpointNodesLastUpdate as DateTime?, + duniterIndexerNodesLastUpdate: + duniterIndexerNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.duniterIndexerNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : duniterIndexerNodesLastUpdate as DateTime?, + duniterDataNodesLastUpdate: + duniterDataNodesLastUpdate == const $CopyWithPlaceholder() + ? _value.duniterDataNodesLastUpdate + // ignore: cast_nullable_to_non_nullable + : duniterDataNodesLastUpdate as DateTime?, + ipfsGatewaysLastUpdate: + ipfsGatewaysLastUpdate == const $CopyWithPlaceholder() + ? _value.ipfsGatewaysLastUpdate + // ignore: cast_nullable_to_non_nullable + : ipfsGatewaysLastUpdate as DateTime?, isLoading: isLoading == const $CopyWithPlaceholder() ? _value.isLoading // ignore: cast_nullable_to_non_nullable @@ -169,6 +264,28 @@ NodeListState _$NodeListStateFromJson(Map<String, dynamic> json) => ipfsGateways: NodeListState._nodesFromJson(json['ipfsGateways'] as List), currentGvaNode: NodeListState._nodeFromJson( json['currentGvaNode'] as Map<String, dynamic>?), + duniterNodesLastUpdate: json['duniterNodesLastUpdate'] == null + ? null + : DateTime.parse(json['duniterNodesLastUpdate'] as String), + cesiumPlusNodesLastUpdate: json['cesiumPlusNodesLastUpdate'] == null + ? null + : DateTime.parse(json['cesiumPlusNodesLastUpdate'] as String), + gvaNodesLastUpdate: json['gvaNodesLastUpdate'] == null + ? null + : DateTime.parse(json['gvaNodesLastUpdate'] as String), + endpointNodesLastUpdate: json['endpointNodesLastUpdate'] == null + ? null + : DateTime.parse(json['endpointNodesLastUpdate'] as String), + duniterIndexerNodesLastUpdate: + json['duniterIndexerNodesLastUpdate'] == null + ? null + : DateTime.parse(json['duniterIndexerNodesLastUpdate'] as String), + duniterDataNodesLastUpdate: json['duniterDataNodesLastUpdate'] == null + ? null + : DateTime.parse(json['duniterDataNodesLastUpdate'] as String), + ipfsGatewaysLastUpdate: json['ipfsGatewaysLastUpdate'] == null + ? null + : DateTime.parse(json['ipfsGatewaysLastUpdate'] as String), isLoading: json['isLoading'] as bool?, ); @@ -182,6 +299,19 @@ Map<String, dynamic> _$NodeListStateToJson(NodeListState instance) => NodeListState._nodesToJson(instance.duniterIndexerNodes), 'duniterDataNodes': NodeListState._nodesToJson(instance.duniterDataNodes), 'ipfsGateways': NodeListState._nodesToJson(instance.ipfsGateways), + 'duniterNodesLastUpdate': + instance.duniterNodesLastUpdate?.toIso8601String(), + 'cesiumPlusNodesLastUpdate': + instance.cesiumPlusNodesLastUpdate?.toIso8601String(), + 'gvaNodesLastUpdate': instance.gvaNodesLastUpdate?.toIso8601String(), + 'endpointNodesLastUpdate': + instance.endpointNodesLastUpdate?.toIso8601String(), + 'duniterIndexerNodesLastUpdate': + instance.duniterIndexerNodesLastUpdate?.toIso8601String(), + 'duniterDataNodesLastUpdate': + instance.duniterDataNodesLastUpdate?.toIso8601String(), + 'ipfsGatewaysLastUpdate': + instance.ipfsGatewaysLastUpdate?.toIso8601String(), 'isLoading': instance.isLoading, 'currentGvaNode': NodeListState._nodeToJson(instance.currentGvaNode), }; diff --git a/lib/ui/screens/node_list_page.dart b/lib/ui/screens/node_list_page.dart index 6bfc834530bdc25a5caf85792251f8e40122e711..2f4d79d4cf02af4a13011eeb29321a6df013e353 100644 --- a/lib/ui/screens/node_list_page.dart +++ b/lib/ui/screens/node_list_page.dart @@ -63,6 +63,7 @@ class NodeListPage extends StatelessWidget { children: <Widget>[ NodeListHeader( type: NodeType.endpoint, + lastUpdated: state.endpointNodesLastUpdate, nodesCount: endPointNodes.length), if (endPointNodes.isNotEmpty) NodeListWidget( @@ -71,6 +72,7 @@ class NodeListPage extends StatelessWidget { currentBlock: endPointNodes[0].currentBlock), NodeListHeader( type: NodeType.duniterIndexer, + lastUpdated: state.duniterIndexerNodesLastUpdate, nodesCount: defaultDuniterIndexerNodes.length), if (duniterIndexerNodes.isNotEmpty) NodeListWidget( @@ -79,6 +81,7 @@ class NodeListPage extends StatelessWidget { currentBlock: duniterIndexerNodes[0].currentBlock), NodeListHeader( type: NodeType.datapodEndpoint, + lastUpdated: state.duniterDataNodesLastUpdate, nodesCount: defaultDatapodEndpointNodes.length), if (duniterDataNodes.isNotEmpty) NodeListWidget( @@ -87,6 +90,7 @@ class NodeListPage extends StatelessWidget { currentBlock: duniterDataNodes[0].currentBlock), NodeListHeader( type: NodeType.ipfsGateway, + lastUpdated: state.ipfsGatewaysLastUpdate, nodesCount: defaultIpfsGateways.length), if (ipfsGateways.isNotEmpty) NodeListWidget( @@ -94,7 +98,9 @@ class NodeListPage extends StatelessWidget { type: NodeType.ipfsGateway, currentBlock: ipfsGateways[0].currentBlock), NodeListHeader( - type: NodeType.gva, nodesCount: gvaNodes.length), + type: NodeType.gva, + lastUpdated: state.gvaNodesLastUpdate, + nodesCount: gvaNodes.length), if (gvaNodes.isNotEmpty) NodeListWidget( nodes: gvaNodes, @@ -102,6 +108,7 @@ class NodeListPage extends StatelessWidget { currentBlock: gvaNodes[0].currentBlock), NodeListHeader( type: NodeType.duniter, + lastUpdated: state.duniterNodesLastUpdate, nodesCount: duniterNodes.length, ), if (duniterNodes.isNotEmpty) @@ -111,6 +118,7 @@ class NodeListPage extends StatelessWidget { currentBlock: duniterNodes[0].currentBlock), NodeListHeader( type: NodeType.cesiumPlus, + lastUpdated: state.cesiumPlusNodesLastUpdate, nodesCount: cesiumPlusNodes.length), if (cesiumPlusNodes.isNotEmpty) NodeListWidget( @@ -127,29 +135,50 @@ class NodeListPage extends StatelessWidget { class NodeListHeader extends StatelessWidget { const NodeListHeader( - {super.key, required this.type, required this.nodesCount}); + {super.key, + required this.type, + required this.nodesCount, + this.lastUpdated}); final NodeType type; final int nodesCount; + final DateTime? lastUpdated; @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 ($nodesCount)', - style: const TextStyle(fontSize: 20)), - GestureDetector( - onLongPress: () => _fetchNodes(context, true, type), - child: IconButton( - icon: const Icon(Icons.refresh), - // Force in all cases - onPressed: () => _fetchNodes(context, false, type)), - ) - ], - )); + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Text( + tr('nodes_list_title', + args: <String>[capitalize(type.name), '$nodesCount']), + style: const TextStyle(fontSize: 20), + ), + if (lastUpdated != null) + Text( + tr('nodes_list_last_updated', args: <String>[ + humanizeTime(lastUpdated!, context.locale.toString()) + ]), + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + GestureDetector( + onLongPress: () => _fetchNodes(context, true, type), + child: IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => _fetchNodes(context, false, type), + ), + ), + ], + ), + ); } void _fetchNodes(BuildContext context, bool force, NodeType type) {