From 07bd3dcb9ad5173f96be019cda8a1f78b8073635 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sat, 25 Nov 2023 19:03:26 +0100 Subject: [PATCH] Added top upstreams and average processing time --- lib/config/home_top_items_default_order.dart | 4 +- lib/constants/enums.dart | 2 +- lib/l10n/app_en.arb | 4 +- lib/l10n/app_es.arb | 4 +- lib/models/dns_statistics.dart | 8 + lib/providers/app_config_provider.dart | 13 +- lib/providers/status_provider.dart | 2 +- lib/screens/home/home.dart | 47 +++- lib/screens/home/top_items/row_item.dart | 21 +- .../top_items/top_item_expansion_panel.dart | 193 ++++++++++++++++ lib/screens/home/top_items/top_items.dart | 208 ++++++++---------- .../reorderable_top_items_home.dart | 53 +++-- lib/screens/top_items/top_items.dart | 29 ++- lib/screens/top_items/top_items_modal.dart | 29 ++- lib/widgets/domain_options.dart | 12 +- 15 files changed, 441 insertions(+), 188 deletions(-) create mode 100644 lib/screens/home/top_items/top_item_expansion_panel.dart diff --git a/lib/config/home_top_items_default_order.dart b/lib/config/home_top_items_default_order.dart index 5220987..a5a23f0 100644 --- a/lib/config/home_top_items_default_order.dart +++ b/lib/config/home_top_items_default_order.dart @@ -5,7 +5,9 @@ import 'package:adguard_home_manager/constants/enums.dart'; final List homeTopItemsDefaultOrder = [ HomeTopItems.queriedDomains, HomeTopItems.blockedDomains, - HomeTopItems.recurrentClients + HomeTopItems.recurrentClients, + HomeTopItems.topUpstreams, + HomeTopItems.avgUpstreamResponseTime ]; final String homeTopItemsDefaultOrderString = jsonEncode( diff --git a/lib/constants/enums.dart b/lib/constants/enums.dart index fed648e..f54e665 100644 --- a/lib/constants/enums.dart +++ b/lib/constants/enums.dart @@ -1,2 +1,2 @@ enum LoadStatus { loading, loaded, error } -enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients } \ No newline at end of file +enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c126ba..b9ba629 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -686,5 +686,7 @@ "unsupportedServerVersion": "Unsupported server version", "unsupportedServerVersionMessage": "Your AdGuard Home server version is too old and is not supported by AdGuard Home Manager. You will need to upgrade your AdGuard Home server to a newer version to use this application.", "yourVersion": "Your version: {version}", - "minimumRequiredVersion": "Minimum required version: {version}" + "minimumRequiredVersion": "Minimum required version: {version}", + "topUpstreams": "Top upstreams", + "averageUpstreamResponseTime": "Average upstream response time" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9033460..0e1566c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -686,5 +686,7 @@ "unsupportedServerVersion": "Versión del servidor no soportada", "unsupportedServerVersionMessage": "La versión de tu servidor AdGuard Home es demasiado antigua y no está soportada por AdGuard Home Manager. Necesitarás actualizar tu servidor AdGuard Home a una versión más actual para utilizar esta aplicación.", "yourVersion": "Tu versión: {version}", - "minimumRequiredVersion": "Versión mínima requerida: {version}" + "minimumRequiredVersion": "Versión mínima requerida: {version}", + "topUpstreams": "DNS de subida más frecuentes", + "averageUpstreamResponseTime": "Tiempo promedio de respuesta upstream" } \ No newline at end of file diff --git a/lib/models/dns_statistics.dart b/lib/models/dns_statistics.dart index 45993bd..f279886 100644 --- a/lib/models/dns_statistics.dart +++ b/lib/models/dns_statistics.dart @@ -9,6 +9,8 @@ class DnsStatistics { final List> topQueriedDomains; final List> topClients; final List> topBlockedDomains; + final List>? topUpstreamResponses; + final List>? topUpstreamsAvgTime; final List dnsQueries; final List blockedFiltering; final List replacedSafebrowsing; @@ -25,6 +27,8 @@ class DnsStatistics { required this.topQueriedDomains, required this.topClients, required this.topBlockedDomains, + required this.topUpstreamResponses, + required this.topUpstreamsAvgTime, required this.dnsQueries, required this.blockedFiltering, required this.replacedSafebrowsing, @@ -42,6 +46,8 @@ class DnsStatistics { topQueriedDomains: List>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), topClients: List>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), topBlockedDomains: List>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + topUpstreamResponses: json["top_upstreams_responses"] != null ? List>.from(json["top_upstreams_responses"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + topUpstreamsAvgTime: json["top_upstreams_avg_time"] != null ? List>.from(json["top_upstreams_avg_time"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, dnsQueries: List.from(json["dns_queries"].map((x) => x)), blockedFiltering: List.from(json["blocked_filtering"].map((x) => x)), replacedSafebrowsing: List.from(json["replaced_safebrowsing"].map((x) => x)), @@ -59,6 +65,8 @@ class DnsStatistics { "top_queried_domains": List.from(topQueriedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_clients": List.from(topClients.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_blocked_domains": List.from(topBlockedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + "top_upstreams_responses": topUpstreamResponses != null ? List.from(topUpstreamResponses!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + "top_upstreams_avg_time": topUpstreamsAvgTime != null ? List.from(topUpstreamsAvgTime!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, "dns_queries": List.from(dnsQueries.map((x) => x)), "blocked_filtering": List.from(blockedFiltering.map((x) => x)), "replaced_safebrowsing": List.from(replacedSafebrowsing.map((x) => x)), diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 16e855f..d44b5af 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -449,7 +449,7 @@ class AppConfigProvider with ChangeNotifier { _showTopItemsChart = dbData['showTopItemsChart']; if (dbData['homeTopItemsOrder'] != null) { try { - _homeTopItemsOrder = List.from( + final itemsOrder = List.from( List.from(jsonDecode(dbData['homeTopItemsOrder'])).map((e) { switch (e) { case 'queriedDomains': @@ -461,11 +461,22 @@ class AppConfigProvider with ChangeNotifier { case 'recurrentClients': return HomeTopItems.recurrentClients; + case 'topUpstreams': + return HomeTopItems.topUpstreams; + + case 'avgUpstreamResponseTime': + return HomeTopItems.avgUpstreamResponseTime; + default: return null; } }).where((e) => e != null).toList() ); + final missingItems = homeTopItemsDefaultOrder.where((e) => !itemsOrder.contains(e)); + _homeTopItemsOrder = [ + ...itemsOrder, + ...missingItems + ]; } catch (e) { Sentry.captureException(e); _homeTopItemsOrder = homeTopItemsDefaultOrder; diff --git a/lib/providers/status_provider.dart b/lib/providers/status_provider.dart index 0c519f8..527749f 100644 --- a/lib/providers/status_provider.dart +++ b/lib/providers/status_provider.dart @@ -237,7 +237,7 @@ class StatusProvider with ChangeNotifier { } Future getServerStatus({ - bool? withLoadingIndicator, + bool? withLoadingIndicator = true, bool? overrideCheckServerVersion }) async { if (withLoadingIndicator == true) { diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 343d065..ec48e54 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -32,8 +32,11 @@ class _HomeState extends State { late bool isVisible; @override - initState(){ - Provider.of(context, listen: false).getServerStatus(); + initState() { + final statusProvider = Provider.of(context, listen: false); + statusProvider.getServerStatus( + withLoadingIndicator: statusProvider.serverStatus != null ? false : true + ); super.initState(); @@ -239,9 +242,9 @@ class TopItemsLists extends StatelessWidget { final List order; const TopItemsLists({ - Key? key, + super.key, required this.order, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -266,8 +269,7 @@ class TopItemsLists extends StatelessWidget { children: [ TopItems( label: AppLocalizations.of(context)!.topQueriedDomains, - data: statusProvider.serverStatus!.stats.topQueriedDomains, - type: 'topQueriedDomains', + type: HomeTopItems.queriedDomains, ), if (item.key < order.length - 1) ...bottom ], @@ -278,8 +280,7 @@ class TopItemsLists extends StatelessWidget { children: [ TopItems( label: AppLocalizations.of(context)!.topBlockedDomains, - data: statusProvider.serverStatus!.stats.topBlockedDomains, - type: 'topBlockedDomains', + type: HomeTopItems.blockedDomains, ), if (item.key < order.length - 1) ...bottom ], @@ -290,13 +291,37 @@ class TopItemsLists extends StatelessWidget { children: [ TopItems( label: AppLocalizations.of(context)!.topClients, - data: statusProvider.serverStatus!.stats.topClients, - type: 'topClients', - clients: true, + type: HomeTopItems.recurrentClients, ), if (item.key < order.length - 1) ...bottom ], ); + + case HomeTopItems.topUpstreams: + return statusProvider.serverStatus!.stats.topUpstreamResponses != null + ? Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.topUpstreams, + type: HomeTopItems.topUpstreams, + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); + + case HomeTopItems.avgUpstreamResponseTime: + return statusProvider.serverStatus!.stats.topUpstreamsAvgTime != null + ? Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.averageUpstreamResponseTime, + type: HomeTopItems.avgUpstreamResponseTime, + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); default: return const SizedBox(); diff --git a/lib/screens/home/top_items/row_item.dart b/lib/screens/home/top_items/row_item.dart index dbbd6d4..4e3c184 100644 --- a/lib/screens/home/top_items/row_item.dart +++ b/lib/screens/home/top_items/row_item.dart @@ -4,28 +4,31 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/domain_options.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; class RowItem extends StatefulWidget { - final String type; + final HomeTopItems type; final Color chartColor; final String domain; final String number; final bool clients; final bool showColor; + final String? unit; const RowItem({ - Key? key, + super.key, required this.type, required this.chartColor, required this.domain, required this.number, required this.clients, required this.showColor, - }) : super(key: key); + this.unit, + }); @override State createState() => _RowItemState(); @@ -93,10 +96,10 @@ class _RowItemState extends State with TickerProviderStateMixin { color: Colors.transparent, child: DomainOptions( item: widget.domain, - isClient: widget.type == 'topClients', - isBlocked: widget.type == 'topBlockedDomains', + isDomain: widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains, + isBlocked: widget.type == HomeTopItems.blockedDomains, onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { + if (widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains) { logsProvider.setSearchText(widget.domain); logsProvider.setSelectedClients(null); logsProvider.setAppliedFilters( @@ -108,7 +111,7 @@ class _RowItemState extends State with TickerProviderStateMixin { ); appConfigProvider.setSelectedScreen(2); } - else if (widget.type == 'topClients') { + else if (widget.type == HomeTopItems.recurrentClients) { logsProvider.setSearchText(null); logsProvider.setSelectedClients([widget.domain]); logsProvider.setAppliedFilters( @@ -195,10 +198,10 @@ class OthersRowItem extends StatefulWidget { final bool showColor; const OthersRowItem({ - Key? key, + super.key, required this.items, required this.showColor, - }) : super(key: key); + }); @override State createState() => _OthersRowItemState(); diff --git a/lib/screens/home/top_items/top_item_expansion_panel.dart b/lib/screens/home/top_items/top_item_expansion_panel.dart new file mode 100644 index 0000000..eb62d01 --- /dev/null +++ b/lib/screens/home/top_items/top_item_expansion_panel.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; +import 'package:adguard_home_manager/screens/home/top_items/row_item.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; + +class TopItemExpansionPanel extends StatefulWidget { + final HomeTopItems type; + final String label; + final List> data; + final Map chartData; + final bool withChart; + + const TopItemExpansionPanel({ + super.key, + required this.type, + required this.label, + required this.data, + required this.chartData, + required this.withChart + }); + + @override + State createState() => _TopItemExpansionPanelState(); +} + +class _TopItemExpansionPanelState extends State { + bool _showChart = true; + + final colors = [ + Colors.red, + Colors.green, + Colors.blue, + Colors.orange, + Colors.teal, + Colors.grey + ]; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + if (widget.withChart == true) { + return Column( + children: [ + ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + SizedBox( + height: 150, + child: CustomPieChart( + data: widget.chartData, + colors: colors + ) + ), + const SizedBox(height: 16), + ], + ), + ), + isExpanded: _showChart + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: _showChart, + unit: widget.type == HomeTopItems.avgUpstreamResponseTime ? 'ms' : null, + ), + ), + if (widget.type != HomeTopItems.avgUpstreamResponseTime) OthersRowItem( + items: widget.data, + showColor: _showChart, + ), + const SizedBox(height: 16), + ], + ); + } + else { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: false, + unit: widget.type == HomeTopItems.avgUpstreamResponseTime ? 'ms' : null, + ), + ), + if (widget.type != HomeTopItems.avgUpstreamResponseTime) OthersRowItem( + items: widget.data, + showColor: false, + ), + const SizedBox(height: 16), + ], + ); + } + } +} + +class _ItemsList extends StatelessWidget { + final List colors; + final List> data; + final bool? clients; + final HomeTopItems type; + final bool showChart; + final String? unit; + + const _ItemsList({ + required this.colors, + required this.data, + required this.clients, + required this.type, + required this.showChart, + this.unit, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: data.sublist( + 0, data.length > 5 ? 5 : data.length + ).asMap().entries.map((e) => RowItem( + clients: clients ?? false, + domain: e.value.keys.toList()[0], + number: e.value.values.toList()[0].runtimeType == double + ? "${e.value.values.toList()[0].toStringAsFixed(2)}${unit != null ? ' $unit' : ''}" + : "${e.value.values.toList()[0].toString()}${unit != null ? ' $unit' : ''}", + type: type, + chartColor: colors[e.key], + showColor: showChart, + )).toList() + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/top_items/top_items.dart b/lib/screens/home/top_items/top_items.dart index 5fd6435..6276d74 100644 --- a/lib/screens/home/top_items/top_items.dart +++ b/lib/screens/home/top_items/top_items.dart @@ -7,26 +7,24 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/home/top_items/row_item.dart'; -import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; +import 'package:adguard_home_manager/screens/home/top_items/top_item_expansion_panel.dart'; import 'package:adguard_home_manager/screens/top_items/top_items_modal.dart'; import 'package:adguard_home_manager/screens/top_items/top_items.dart'; +import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItems extends StatefulWidget { - final String type; + final HomeTopItems type; final String label; - final List> data; - final bool? clients; const TopItems({ - Key? key, + super.key, required this.type, required this.label, - required this.data, - this.clients - }) : super(key: key); + }); @override State createState() => _TopItemsState(); @@ -58,31 +56,41 @@ class _TopItemsState extends State { List> generateData() { switch (widget.type) { - case 'topQueriedDomains': + case HomeTopItems.queriedDomains: return statusProvider.serverStatus!.stats.topQueriedDomains; - case 'topBlockedDomains': + case HomeTopItems.blockedDomains: return statusProvider.serverStatus!.stats.topBlockedDomains; - case 'topClients': + case HomeTopItems.recurrentClients: return statusProvider.serverStatus!.stats.topClients; + case HomeTopItems.topUpstreams: + return statusProvider.serverStatus!.stats.topUpstreamResponses ?? []; + + case HomeTopItems.avgUpstreamResponseTime: + return statusProvider.serverStatus!.stats.topUpstreamsAvgTime ?? []; + default: return []; } } + final data = generateData(); + + final withChart = widget.type != HomeTopItems.avgUpstreamResponseTime; + Map chartData() { Map values = {}; - widget.data.sublist(0, widget.data.length > 5 ? 5 : widget.data.length).forEach((element) { + data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) { values = { ...values, element.keys.first: element.values.first.toDouble() }; }); - if (widget.data.length > 5) { + if (data.length > 5) { final int rest = List.from( - widget.data.sublist(5, widget.data.length).map((e) => e.values.first.toInt()) + data.sublist(5, data.length).map((e) => e.values.first.toInt()) ).reduce((a, b) => a + b); values = { ...values, @@ -109,108 +117,71 @@ class _TopItemsState extends State { return SizedBox( child: Column( children: [ - if (widget.data.isEmpty) noItems, - if (widget.data.isNotEmpty && width > 700) Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 1, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 250 - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: CustomPieChart( - data: chartData(), - colors: colors - ) - ), - ) - ), - Expanded( - flex: 2, - child: Column( - children: [ - ItemsList( - colors: colors, - data: widget.data, - clients: widget.clients, - type: widget.type, - showChart: _showChart - ), - OthersRowItem( - items: widget.data, - showColor: true, - ) - ] - ), - ) - ], - ), - if (widget.data.isNotEmpty && width <= 700) ...[ - ExpansionPanelList( - expandedHeaderPadding: const EdgeInsets.all(0), - elevation: 0, - expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded), - animationDuration: const Duration(milliseconds: 250), + if (data.isEmpty) noItems, + if (data.isNotEmpty && width > 700) Padding( + padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ExpansionPanel( - headerBuilder: (context, isExpanded) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: width <= 700 - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, - children: [ - Text( + if (withChart == true) Expanded( + flex: 1, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 250 + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: CustomPieChart( + data: chartData(), + colors: colors + ) + ), + ) + ), + Expanded( + flex: 2, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 16 + ), + child: Text( widget.label, - style: TextStyle( + style: const TextStyle( fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface + fontWeight: FontWeight.w500 ), ), - ], - ), + ), + _ItemsList( + colors: colors, + data: data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: withChart == true ? _showChart : false, + unit: widget.type == HomeTopItems.avgUpstreamResponseTime ? 'ms' : null, + ), + if (withChart == true) OthersRowItem( + items: data, + showColor: true, + ) + ] ), - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - children: [ - SizedBox( - height: 150, - child: CustomPieChart( - data: chartData(), - colors: colors - ) - ), - const SizedBox(height: 16), - ], - ), - ), - isExpanded: _showChart - ), + ) ], ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: ItemsList( - colors: colors, - data: widget.data, - clients: widget.clients, - type: widget.type, - showChart: _showChart - ), - ), - OthersRowItem( - items: widget.data, - showColor: _showChart, - ), - const SizedBox(height: 16), - ], + ), + if (data.isNotEmpty && width <= 700) TopItemExpansionPanel( + type: widget.type, + label: widget.label, + data: data, + chartData: chartData(), + withChart: withChart + ), - if (widget.data.length > 5) ...[ + if (data.length > 5) ...[ Padding( padding: const EdgeInsets.only(right: 20), child: Row( @@ -225,8 +196,10 @@ class _TopItemsState extends State { builder: (context) => TopItemsModal( type: widget.type, title: widget.label, - isClient: widget.clients, + isClient: widget.type == HomeTopItems.recurrentClients, data: generateData(), + withProgressBar: widget.type != HomeTopItems.avgUpstreamResponseTime, + unit: widget.type == HomeTopItems.avgUpstreamResponseTime ? 'ms' : null, ) ) } @@ -236,8 +209,10 @@ class _TopItemsState extends State { builder: (context) => TopItemsScreen( type: widget.type, title: widget.label, - isClient: widget.clients, + isClient: widget.type == HomeTopItems.recurrentClients, data: generateData(), + withProgressBar: widget.type != HomeTopItems.avgUpstreamResponseTime, + unit: widget.type == HomeTopItems.avgUpstreamResponseTime ? 'ms' : null, ) ) ) @@ -266,21 +241,22 @@ class _TopItemsState extends State { } } -class ItemsList extends StatelessWidget { +class _ItemsList extends StatelessWidget { final List colors; final List> data; final bool? clients; - final String type; + final HomeTopItems type; final bool showChart; + final String? unit; - const ItemsList({ - Key? key, + const _ItemsList({ required this.colors, required this.data, required this.clients, required this.type, required this.showChart, - }) : super(key: key); + this.unit, + }); @override Widget build(BuildContext context) { @@ -290,7 +266,9 @@ class ItemsList extends StatelessWidget { ).asMap().entries.map((e) => RowItem( clients: clients ?? false, domain: e.value.keys.toList()[0], - number: e.value.values.toList()[0].toString(), + number: e.value.values.toList()[0].runtimeType == double + ? "${e.value.values.toList()[0].toStringAsFixed(2)}${unit != null ? ' $unit' : ''}" + : "${e.value.values.toList()[0].toString()}${unit != null ? ' $unit' : ''}", type: type, chartColor: colors[e.key], showColor: showChart, diff --git a/lib/screens/settings/general_settings/reorderable_top_items_home.dart b/lib/screens/settings/general_settings/reorderable_top_items_home.dart index b649ff5..2b7bfe1 100644 --- a/lib/screens/settings/general_settings/reorderable_top_items_home.dart +++ b/lib/screens/settings/general_settings/reorderable_top_items_home.dart @@ -1,6 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -9,15 +8,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class ItemData { +class _ItemData { final HomeTopItems title; final Key key; - const ItemData({ + const _ItemData({ required this.title, required this.key }); @@ -29,7 +29,7 @@ enum DraggingMode { } class ReorderableTopItemsHome extends StatefulWidget { - const ReorderableTopItemsHome({Key? key}) : super(key: key); + const ReorderableTopItemsHome({super.key}); @override State createState() => _ReorderableTopItemsHomeState(); @@ -38,10 +38,10 @@ class ReorderableTopItemsHome extends StatefulWidget { class _ReorderableTopItemsHomeState extends State { List homeTopItemsList = []; List persistHomeTopItemsList = []; - List renderItems = []; + List<_ItemData> renderItems = []; int _indexOfKey(Key key) { - return renderItems.indexWhere((ItemData d) => d.key == key); + return renderItems.indexWhere((_ItemData d) => d.key == key); } bool _reorderCallback(Key item, Key newPosition) { @@ -79,7 +79,7 @@ class _ReorderableTopItemsHomeState extends State { homeTopItemsList = appConfigProvider.homeTopItemsOrder; persistHomeTopItemsList = appConfigProvider.homeTopItemsOrder; renderItems = appConfigProvider.homeTopItemsOrder.asMap().entries.map( - (e) => ItemData( + (e) => _ItemData( key: ValueKey(e.key), title: e.value, ) @@ -117,16 +117,31 @@ class _ReorderableTopItemsHomeState extends State { padding: const EdgeInsets.all(16) ); + case HomeTopItems.topUpstreams: + return CustomListTile( + title: AppLocalizations.of(context)!.topUpstreams, + icon: Icons.upload_file_rounded, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.avgUpstreamResponseTime: + return CustomListTile( + title: AppLocalizations.of(context)!.averageUpstreamResponseTime, + icon: Icons.timer_rounded, + padding: const EdgeInsets.all(16) + ); + default: return const SizedBox(); } } - Future onWillPopScope() async { + Future onWillPopScope(bool popInvoked) async { if (!listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList)) { - showDialog( + await showDialog( context: context, - builder: (dialogContext) => AlertDialog( + useRootNavigator: false, + builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(context)!.discardChanges), content: Text(AppLocalizations.of(context)!.discardChangesDescription), actions: [ @@ -135,14 +150,14 @@ class _ReorderableTopItemsHomeState extends State { children: [ TextButton( onPressed: () { - Navigator.pop(dialogContext); + Navigator.pop(context); Navigator.pop(context); }, child: Text(AppLocalizations.of(context)!.confirm) ), const SizedBox(width: 8), TextButton( - onPressed: () => Navigator.pop(dialogContext), + onPressed: () => Navigator.pop(context), child: Text(AppLocalizations.of(context)!.cancel) ), ], @@ -175,8 +190,9 @@ class _ReorderableTopItemsHomeState extends State { } } - return WillPopScope( - onWillPop: onWillPopScope, + return PopScope( + canPop: listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList), + onPopInvoked: onWillPopScope, child: Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.topItemsOrder), @@ -219,7 +235,7 @@ class _ReorderableTopItemsHomeState extends State { child: ListView.builder( itemBuilder: (context, index) => reorderable_list_library.ReorderableItem( key: renderItems[index].key, - childBuilder: (context, state) => Item( + childBuilder: (context, state) => _Item( tileWidget: tile(renderItems[index].title), isFirst: index == 0, isLast: index == renderItems.length - 1, @@ -237,19 +253,18 @@ class _ReorderableTopItemsHomeState extends State { } } -class Item extends StatelessWidget { +class _Item extends StatelessWidget { final Widget tileWidget; final bool isFirst; final bool isLast; final reorderable_list_library.ReorderableItemState state; - const Item({ - Key? key, + const _Item({ required this.tileWidget, required this.isFirst, required this.isLast, required this.state, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/top_items/top_items.dart b/lib/screens/top_items/top_items.dart index 26b84d5..66ca288 100644 --- a/lib/screens/top_items/top_items.dart +++ b/lib/screens/top_items/top_items.dart @@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/domain_options.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; @@ -18,18 +19,22 @@ import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItemsScreen extends StatefulWidget { - final String type; + final HomeTopItems type; final String title; final bool? isClient; final List> data; + final bool withProgressBar; + final String? unit; const TopItemsScreen({ - Key? key, + super.key, required this.type, required this.title, this.isClient, required this.data, - }) : super(key: key); + required this.withProgressBar, + this.unit, + }); @override State createState() => _TopItemsScreenState(); @@ -60,9 +65,9 @@ class _TopItemsScreenState extends State { final appConfigProvider = Provider.of(context); final logsProvider = Provider.of(context); - int total = 0; + double total = 0; for (var element in data) { - total = total + int.parse(element.values.toList()[0].toString()); + total = total + double.parse(element.values.toList()[0].toString()); } return Scaffold( @@ -154,10 +159,10 @@ class _TopItemsScreenState extends State { return DomainOptions( item: screenData[index].keys.toList()[0], - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', + isBlocked: widget.type == HomeTopItems.blockedDomains, + isDomain: widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains, onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { + if (widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains) { logsProvider.setSearchText(screenData[index].keys.toList()[0]); logsProvider.setSelectedClients(null); logsProvider.setAppliedFilters( @@ -170,7 +175,7 @@ class _TopItemsScreenState extends State { appConfigProvider.setSelectedScreen(2); Navigator.pop(context); } - else if (widget.type == 'topClients') { + else if (widget.type == HomeTopItems.recurrentClients) { logsProvider.setSearchText(null); logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); logsProvider.setAppliedFilters( @@ -187,7 +192,9 @@ class _TopItemsScreenState extends State { child: CustomListTile( title: screenData[index].keys.toList()[0], trailing: Text( - screenData[index].values.toList()[0].toString(), + screenData[index].values.toList()[0].runtimeType == double + ? "${screenData[index].values.toList()[0].toStringAsFixed(2)}${widget.unit != null ? ' ${widget.unit}' : ''}" + : "${screenData[index].values.toList()[0].toString()}${widget.unit != null ? ' ${widget.unit}' : ''}", style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant ), @@ -205,7 +212,7 @@ class _TopItemsScreenState extends State { ), const SizedBox(height: 5), ], - Row( + if (widget.withProgressBar == true) Row( children: [ SizedBox( width: 50, diff --git a/lib/screens/top_items/top_items_modal.dart b/lib/screens/top_items/top_items_modal.dart index 845459a..1325c79 100644 --- a/lib/screens/top_items/top_items_modal.dart +++ b/lib/screens/top_items/top_items_modal.dart @@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/domain_options.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; @@ -17,18 +18,22 @@ import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItemsModal extends StatefulWidget { - final String type; + final HomeTopItems type; final String title; final bool? isClient; final List> data; + final bool withProgressBar; + final String? unit; const TopItemsModal({ - Key? key, + super.key, required this.type, required this.title, this.isClient, required this.data, - }) : super(key: key); + required this.withProgressBar, + this.unit, + }); @override State createState() => _TopItemsModalState(); @@ -59,9 +64,9 @@ class _TopItemsModalState extends State { final appConfigProvider = Provider.of(context); final logsProvider = Provider.of(context); - int total = 0; + double total = 0; for (var element in data) { - total = total + int.parse(element.values.toList()[0].toString()); + total = total + double.parse(element.values.toList()[0].toString()); } return Dialog( @@ -129,11 +134,11 @@ class _TopItemsModalState extends State { } return DomainOptions( - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', + isBlocked: widget.type == HomeTopItems.blockedDomains, + isDomain: widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains, item: screenData[index].keys.toList()[0], onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { + if (widget.type == HomeTopItems.queriedDomains || widget.type == HomeTopItems.blockedDomains) { logsProvider.setSearchText(screenData[index].keys.toList()[0]); logsProvider.setSelectedClients(null); logsProvider.setAppliedFilters( @@ -146,7 +151,7 @@ class _TopItemsModalState extends State { appConfigProvider.setSelectedScreen(2); Navigator.pop(context); } - else if (widget.type == 'topClients') { + else if (widget.type == HomeTopItems.recurrentClients) { logsProvider.setSearchText(null); logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); logsProvider.setAppliedFilters( @@ -163,7 +168,9 @@ class _TopItemsModalState extends State { child: CustomListTile( title: screenData[index].keys.toList()[0], trailing: Text( - screenData[index].values.toList()[0].toString(), + screenData[index].values.toList()[0].runtimeType == double + ? "${screenData[index].values.toList()[0].toStringAsFixed(2)}${widget.unit != null ? ' ${widget.unit}' : ''}" + : "${screenData[index].values.toList()[0].toString()}${widget.unit != null ? ' ${widget.unit}' : ''}", style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant ), @@ -181,7 +188,7 @@ class _TopItemsModalState extends State { ), const SizedBox(height: 5), ], - Row( + if (widget.withProgressBar == true) Row( children: [ SizedBox( width: 50, diff --git a/lib/widgets/domain_options.dart b/lib/widgets/domain_options.dart index 3c4bb79..703f63f 100644 --- a/lib/widgets/domain_options.dart +++ b/lib/widgets/domain_options.dart @@ -17,21 +17,21 @@ import 'package:adguard_home_manager/models/menu_option.dart'; class DomainOptions extends StatelessWidget { final bool isBlocked; - final bool? isClient; + final bool? isDomain; final String? item; final Widget child; final void Function() onTap; final BorderRadius? borderRadius; const DomainOptions({ - Key? key, + super.key, required this.isBlocked, - this.isClient, + this.isDomain, required this.item, required this.child, required this.onTap, this.borderRadius - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -80,12 +80,12 @@ class DomainOptions extends StatelessWidget { List generateOptions() { return [ - if (isClient != true && isBlocked == true) MenuOption( + if (isDomain == true && isBlocked == true) MenuOption( title: AppLocalizations.of(context)!.unblock, icon: Icons.check, action: () => blockUnblock(item!, 'unblock') ), - if (isClient != true && isBlocked == false) MenuOption( + if (isDomain == true && isBlocked == false) MenuOption( title: AppLocalizations.of(context)!.block, icon: Icons.block, action: () => blockUnblock(item!, 'block')