From a4a7840e55d73bcc24d4e858d29375290d38f795 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sat, 4 Feb 2023 14:56:57 +0100 Subject: [PATCH] Added modal to select clients on logs --- lib/l10n/app_en.arb | 3 +- lib/l10n/app_es.arb | 3 +- lib/models/applied_filters.dart | 4 +- lib/providers/logs_provider.dart | 34 ++++- lib/screens/home/top_items.dart | 3 +- lib/screens/logs/clients_modal.dart | 172 +++++++++++++++++++++++ lib/screens/logs/logs.dart | 27 +++- lib/screens/logs/logs_filters_modal.dart | 43 +++++- lib/screens/top_items/top_items.dart | 3 +- lib/widgets/custom_list_tile.dart | 12 +- 10 files changed, 291 insertions(+), 13 deletions(-) create mode 100644 lib/screens/logs/clients_modal.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0b520a0..e3e57a0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -580,5 +580,6 @@ "clearDnsCacheMessage": "Are you sure you want to clear the DNS cache?", "dnsCacheCleared": "DNS cache cleared successfully", "clearingDnsCache": "Clearing cache...", - "dnsCacheNotCleared": "DNS cache couldn't be cleared" + "dnsCacheNotCleared": "DNS cache couldn't be cleared", + "clientsSelected": "clients selected" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3bcb369..7aa1e2a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -580,5 +580,6 @@ "clearDnsCacheMessage": "¿Estás seguro que deseas eliminar la caché de DNS?", "dnsCacheCleared": "Caché de DNS borrada correctamente", "clearingDnsCache": "Borrando caché...", - "dnsCacheNotCleared": "La caché de DNS no pudo ser borrada" + "dnsCacheNotCleared": "La caché de DNS no pudo ser borrada", + "clientsSelected": "clientes seleccionados" } \ No newline at end of file diff --git a/lib/models/applied_filters.dart b/lib/models/applied_filters.dart index 6d6dc5d..2b60e71 100644 --- a/lib/models/applied_filters.dart +++ b/lib/models/applied_filters.dart @@ -1,9 +1,11 @@ class AppliedFiters { String selectedResultStatus = 'all'; String? searchText; + List? clients; AppliedFiters({ required this.selectedResultStatus, - required this.searchText + required this.searchText, + required this.clients }); } \ No newline at end of file diff --git a/lib/providers/logs_provider.dart b/lib/providers/logs_provider.dart index 086d7d0..db2619c 100644 --- a/lib/providers/logs_provider.dart +++ b/lib/providers/logs_provider.dart @@ -1,3 +1,4 @@ +import 'package:adguard_home_manager/models/clients.dart'; import 'package:flutter/material.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; @@ -6,17 +7,21 @@ import 'package:adguard_home_manager/models/logs.dart'; class LogsProvider with ChangeNotifier { int _loadStatus = 0; LogsData? _logsData; + List? _clients; + int _clientsLoadStatus = 0; DateTime? _logsOlderThan; String _selectedResultStatus = 'all'; String? _searchText; + List? _selectedClients; int _logsQuantity = 100; int _offset = 0; AppliedFiters _appliedFilters = AppliedFiters( selectedResultStatus: 'all', - searchText: null + searchText: null, + clients: null ); int get loadStatus { @@ -27,6 +32,10 @@ class LogsProvider with ChangeNotifier { return _logsData; } + List? get clients { + return _clients; + } + DateTime? get logsOlderThan { return _logsOlderThan; } @@ -47,6 +56,14 @@ class LogsProvider with ChangeNotifier { return _offset; } + List? get selectedClients { + return _selectedClients; + } + + int get clientsLoadStatus { + return _clientsLoadStatus; + } + AppliedFiters get appliedFilters { return _appliedFilters; } @@ -62,6 +79,16 @@ class LogsProvider with ChangeNotifier { notifyListeners(); } + void setClients(List clients) { + _clients = clients; + notifyListeners(); + } + + void setClientsLoadStatus(int status) { + _clientsLoadStatus = status; + notifyListeners(); + } + void setLogsOlderThan(DateTime? value) { _logsOlderThan = value; notifyListeners(); @@ -94,6 +121,11 @@ class LogsProvider with ChangeNotifier { notifyListeners(); } + void setSelectedClients(List? clients) { + _selectedClients = clients; + notifyListeners(); + } + void setAppliedFilters(AppliedFiters value) { _appliedFilters = value; notifyListeners(); diff --git a/lib/screens/home/top_items.dart b/lib/screens/home/top_items.dart index a11f8c2..eec1e26 100644 --- a/lib/screens/home/top_items.dart +++ b/lib/screens/home/top_items.dart @@ -143,7 +143,8 @@ class TopItems extends StatelessWidget { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: 'all', - searchText: item.keys.toList()[0] + searchText: item.keys.toList()[0], + clients: null ) ); appConfigProvider.setSelectedScreen(2); diff --git a/lib/screens/logs/clients_modal.dart b/lib/screens/logs/clients_modal.dart new file mode 100644 index 0000000..c8c157d --- /dev/null +++ b/lib/screens/logs/clients_modal.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +class ClientsModal extends StatefulWidget { + final List? value; + + const ClientsModal({ + Key? key, + required this.value + }) : super(key: key); + + @override + State createState() => _ClientsModalState(); +} + +class _ClientsModalState extends State { + List selectedClients = []; + + @override + void initState() { + setState(() => selectedClients = widget.value ?? []); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final logsProvider = Provider.of(context); + + final height = MediaQuery.of(context).size.height; + + void apply() async { + logsProvider.setSelectedClients( + selectedClients.isNotEmpty ? selectedClients : null + ); + + Navigator.pop(context); + } + + Widget listItem({ + required String label, + required void Function() onChanged + }) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChanged(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Checkbox( + value: selectedClients.contains(label), + onChanged: (_) => onChanged(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ) + ], + ), + ), + ), + ); + } + + void selectAll() { + setState(() { + selectedClients = logsProvider.clients!.map((item) => item.ip).toList(); + }); + } + + void unselectAll() { + setState(() { + selectedClients = []; + }); + } + + return Container( + height: height >= (logsProvider.clients!.length*64) == true + ? logsProvider.clients!.length*64 + : height-25, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 16, + ), + child: Icon( + Icons.smartphone_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.clients, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + physics: height >= (logsProvider.clients!.length*64) == true + ? const NeverScrollableScrollPhysics() + : null, + itemCount: logsProvider.clients!.length, + itemBuilder: (context, index) => listItem( + label: logsProvider.clients![index].ip, + onChanged: () { + if (selectedClients.contains(logsProvider.clients![index].ip)) { + setState(() { + selectedClients = selectedClients.where( + (item) => item != logsProvider.clients![index].ip + ).toList(); + }); + } + else { + setState(() { + selectedClients.add(logsProvider.clients![index].ip); + }); + } + } + ) + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: selectedClients.length == logsProvider.clients!.length + ? () => unselectAll() + : () => selectAll(), + child: Text( + selectedClients.length == logsProvider.clients!.length + ? AppLocalizations.of(context)!.unselectAll + : AppLocalizations.of(context)!.selectAll + ) + ), + TextButton( + onPressed: apply, + child: Text(AppLocalizations.of(context)!.apply) + ) + ], + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/logs.dart b/lib/screens/logs/logs.dart index c8c5a55..05bf65f 100644 --- a/lib/screens/logs/logs.dart +++ b/lib/screens/logs/logs.dart @@ -129,6 +129,26 @@ class _LogsWidgetState extends State { } } + Future fetchClients() async { + final result = await getClients(widget.serversProvider.selectedServer!); + if (mounted) { + if (result['result'] == 'success') { + widget.logsProvider.setClientsLoadStatus(1); + widget.logsProvider.setClients(result['data'].autoClientsData); + } + else { + widget.logsProvider.setClientsLoadStatus(2); + widget.appConfigProvider.addLog(result['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.couldntGetFilteringStatus), + backgroundColor: Colors.red, + ) + ); + } + } + } + void scrollListener() { if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { fetchLogs(loadingMore: true); @@ -146,6 +166,7 @@ class _LogsWidgetState extends State { scrollController = ScrollController()..addListener(scrollListener); fetchLogs(inOffset: 0); fetchFilteringRules(); + fetchClients(); super.initState(); } @@ -417,7 +438,8 @@ class _LogsWidgetState extends State { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - searchText: null + searchText: null, + clients: null ) ); logsProvider.setSearchText(null); @@ -463,7 +485,8 @@ class _LogsWidgetState extends State { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: 'all', - searchText: logsProvider.appliedFilters.searchText + searchText: logsProvider.appliedFilters.searchText, + clients: null ) ); logsProvider.setSelectedResultStatus('all'); diff --git a/lib/screens/logs/logs_filters_modal.dart b/lib/screens/logs/logs_filters_modal.dart index 3a4da44..1c94ff9 100644 --- a/lib/screens/logs/logs_filters_modal.dart +++ b/lib/screens/logs/logs_filters_modal.dart @@ -1,5 +1,6 @@ // ignore_for_file: use_build_context_synchronously +import 'package:adguard_home_manager/screens/logs/clients_modal.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -111,7 +112,8 @@ class _LogsFiltersModalWidgetState extends State { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: 'all', - searchText: null + searchText: null, + clients: null ) ); @@ -136,6 +138,17 @@ class _LogsFiltersModalWidgetState extends State { ); } + void openSelectClients() { + showModalBottomSheet( + context: context, + builder: (context) => ClientsModal( + value: logsProvider.selectedClients, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } + void filterLogs() async { Navigator.pop(context); @@ -155,6 +168,7 @@ class _LogsFiltersModalWidgetState extends State { AppliedFiters( selectedResultStatus: logsProvider.selectedResultStatus, searchText: logsProvider.searchText, + clients: logsProvider.selectedClients ) ); @@ -171,7 +185,7 @@ class _LogsFiltersModalWidgetState extends State { return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( - height: 360, + height: 430, decoration: BoxDecoration( color: Theme.of(context).dialogBackgroundColor, borderRadius: const BorderRadius.only( @@ -282,6 +296,31 @@ class _LogsFiltersModalWidgetState extends State { // ), // ), // ), + CustomListTile( + title: AppLocalizations.of(context)!.client, + subtitle: logsProvider.selectedClients != null + ? "${logsProvider.selectedClients!.length} ${AppLocalizations.of(context)!.clientsSelected}" + : AppLocalizations.of(context)!.all, + onTap: logsProvider.clientsLoadStatus == 1 + ? openSelectClients + : null, + icon: Icons.smartphone_rounded, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + trailing: logsProvider.clientsLoadStatus == 0 + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : logsProvider.clientsLoadStatus == 2 + ? const Icon( + Icons.error_rounded, + color: Colors.red, + ) + : null, + ), CustomListTile( title: AppLocalizations.of(context)!.responseStatus, subtitle: "${translatedString[logsProvider.selectedResultStatus]}", diff --git a/lib/screens/top_items/top_items.dart b/lib/screens/top_items/top_items.dart index 84e8de0..31708dd 100644 --- a/lib/screens/top_items/top_items.dart +++ b/lib/screens/top_items/top_items.dart @@ -203,7 +203,8 @@ class _TopItemsScreenState extends State { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: 'all', - searchText: screenData[index].keys.toList()[0] + searchText: screenData[index].keys.toList()[0], + clients: null ) ); Navigator.pop(context); diff --git a/lib/widgets/custom_list_tile.dart b/lib/widgets/custom_list_tile.dart index edbb53e..bd995e9 100644 --- a/lib/widgets/custom_list_tile.dart +++ b/lib/widgets/custom_list_tile.dart @@ -42,7 +42,9 @@ class CustomListTile extends StatelessWidget { Icon( icon, size: 24, - color: Theme.of(context).listTileTheme.iconColor, + color: onTap != null || onLongPress != null + ? Theme.of(context).listTileTheme.iconColor + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), ), const SizedBox(width: 16), ], @@ -55,7 +57,9 @@ class CustomListTile extends StatelessWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, + color: onTap != null || onLongPress != null + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), ), ), if (subtitle != null || subtitleWidget != null) ...[ @@ -64,7 +68,9 @@ class CustomListTile extends StatelessWidget { if (subtitle != null && subtitleWidget == null) Text( subtitle!, style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, + color: onTap != null || onLongPress != null + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.38), fontSize: 14, fontWeight: FontWeight.w400 ),