From e8d3e14245d1ddfacfc961a2b5f50491a48a32d0 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 9 Oct 2022 01:13:55 +0200 Subject: [PATCH] Added access control settings --- lib/l10n/app_en.arb | 19 +- lib/l10n/app_es.arb | 19 +- lib/screens/clients/blocked_list.dart | 215 ----------- lib/screens/clients/clients.dart | 20 +- lib/screens/clients/fab.dart | 32 +- lib/screens/settings/access_settings.dart | 152 ++++++++ .../add_client_modal.dart} | 93 +++-- lib/screens/settings/clients_list.dart | 355 ++++++++++++++++++ lib/screens/settings/settings.dart | 13 + 9 files changed, 621 insertions(+), 297 deletions(-) delete mode 100644 lib/screens/clients/blocked_list.dart create mode 100644 lib/screens/settings/access_settings.dart rename lib/screens/{clients/block_client_modal.dart => settings/add_client_modal.dart} (58%) create mode 100644 lib/screens/settings/clients_list.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cde95b3..151bb22 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -263,5 +263,22 @@ "serverVersion": "Server version", "serverLanguage": "Server language", "yes": "Yes", - "no": "No" + "no": "No", + "allowedClients": "Allowed clients", + "disallowedClients": "Disallowed clients", + "disallowedDomains": "Disallowed domains", + "accessSettings": "Access settings", + "accessSettingsDescription": "Configure access rules for the server", + "loadingClients": "Loading clients...", + "clientsNotLoaded": "Clients couldn't be loaded.", + "noAllowedClients": "No hay clientes permitidos", + "allowedClientsDescription": "If this list has entries, AdGuard Home will accept requests only from these clients.", + "blockedClientsDescription": "If this list has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in Allowed clients.", + "disallowedDomainsDescription": "AdGuard Home drops DNS queries matching these domains, and these queries don't even appear in the query log.", + "addClientFieldDescription": "CIDRs, IP address, or ClientID", + "clientIdentifier": "Client identifier", + "allowClient": "Allow client", + "disallowClient": "Disallow client", + "noDisallowedDomains": "No disallowed domains", + "domainNotAdded": "The domain couldn't be added" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 120cafe..12048a6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -263,5 +263,22 @@ "serverVersion": "Versión del servidor", "serverLanguage": "Idioma del servidor", "yes": "Sí", - "no": "No" + "no": "No", + "allowedClients": "Clientes permitidos", + "disallowedClients": "Clientes no permitidos", + "disallowedDomains": "Dominios no permitidos", + "accessSettings": "Ajustes de acceso", + "accessSettingsDescription": "Configura reglas de acceso para el servidor", + "loadingClients": "Cargando clientes...", + "clientsNotLoaded": "Los clientes no pudieron ser cargados.", + "noAllowedClients": "No hay clientes permitidos", + "allowedClientsDescription": "Si esta lista tiene entradas, AdGuard Home aceptará peticiones solo de estos clientes.", + "blockedClientsDescription": "Si esta lista tiene entradas, AdGuard Home descartará las peticiones de estos clientes. Este campo será ignorado si hay entradas en clientes permitidos.", + "disallowedDomainsDescription": " AdGuard Home descartará las consultas DNS que coincidan con estos dominios, y estas consultas ni siquiera aparecerán en el registro de consultas", + "addClientFieldDescription": "CIDRs, Dirección IP, o ClientID", + "clientIdentifier": "Identificador de cliente", + "allowClient": "Permitir cliente", + "disallowClient": "No permitir cliente", + "noDisallowedDomains": "No hay dominios no permitidos", + "domainNotAdded": "El dominio no pudo ser añadido" } \ No newline at end of file diff --git a/lib/screens/clients/blocked_list.dart b/lib/screens/clients/blocked_list.dart deleted file mode 100644 index addd70d..0000000 --- a/lib/screens/clients/blocked_list.dart +++ /dev/null @@ -1,215 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; -import 'package:adguard_home_manager/screens/clients/fab.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; - -class BlockedList extends StatefulWidget { - final ScrollController scrollController; - final int loadStatus; - final List data; - final Future Function() fetchClients; - - const BlockedList({ - Key? key, - required this.scrollController, - required this.loadStatus, - required this.data, - required this.fetchClients - }) : super(key: key); - - @override - State createState() => _BlockedListState(); -} - -class _BlockedListState extends State { - late bool isVisible; - - @override - initState(){ - super.initState(); - - isVisible = true; - widget.scrollController.addListener(() { - if (widget.scrollController.position.userScrollDirection == ScrollDirection.reverse) { - if (mounted && isVisible == true) { - setState(() => isVisible = false); - } - } - else { - if (widget.scrollController.position.userScrollDirection == ScrollDirection.forward) { - if (mounted && isVisible == false) { - setState(() => isVisible = true); - } - } - } - }); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void confirmRemoveDomain(String domain) async { - Map> body = { - "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], - "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients.where((client) => client != domain).toList() ?? [], - "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], - }; - - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.removingClient); - - final result = await requestAllowedBlockedClientsHosts(serversProvider.selectedServer!, body); - - processModal.close(); - - if (result['result'] == 'success') { - serversProvider.setAllowedDisallowedClientsBlockedDomains( - ClientsAllowedBlocked( - allowedClients: body['allowed_clients'] ?? [], - disallowedClients: body['disallowed_clients'] ?? [], - blockedHosts: body['blocked_hosts'] ?? [], - ) - ); - } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.clientAnotherList), - backgroundColor: Colors.red, - ) - ); - } - else { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.clientNotRemoved), - backgroundColor: Colors.red, - ) - ); - } - } - - switch (widget.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingStatus, - style: const TextStyle( - fontSize: 22, - color: Colors.grey, - ), - ) - ], - ), - ); - - case 1: - return Stack( - children: [ - if (widget.data.isNotEmpty) ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: widget.data.length, - itemBuilder: (context, index) => ListTile( - title: Text(widget.data[index]), - trailing: IconButton( - onPressed: () => { - showDialog( - context: context, - builder: (context) => RemoveClientModal( - onConfirm: () => confirmRemoveDomain(widget.data[index]), - ) - ) - }, - icon: const Icon(Icons.delete_rounded) - ), - ) - ), - if (widget.data.isEmpty) SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context)!.noBlockedClients, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, - color: Colors.grey - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: widget.fetchClients, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh), - ) - ], - ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: const ClientsFab(tab: 2), - ) - ] - ); - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.errorLoadServerStatus, - style: const TextStyle( - fontSize: 22, - color: Colors.grey, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - - } -} \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index c009969..61270ad 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -1,10 +1,8 @@ -import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart'; -import 'package:adguard_home_manager/screens/clients/blocked_list.dart'; import 'package:adguard_home_manager/screens/clients/added_list.dart'; import 'package:adguard_home_manager/models/app_log.dart'; @@ -77,7 +75,7 @@ class _ClientsWidgetState extends State with TickerProviderStateM super.initState(); tabController = TabController( initialIndex: 0, - length: 3, + length: 2, vsync: this, ); tabController.addListener(() => widget.setSelectedClientsTab(tabController.index)); @@ -92,7 +90,7 @@ class _ClientsWidgetState extends State with TickerProviderStateM final serversProvider = Provider.of(context); return DefaultTabController( - length: 3, + length: 2, child: NestedScrollView( controller: scrollController, headerSliverBuilder: ((context, innerBoxIsScrolled) { @@ -114,10 +112,6 @@ class _ClientsWidgetState extends State with TickerProviderStateM icon: const Icon(Icons.add), text: AppLocalizations.of(context)!.added, ), - Tab( - icon: const Icon(Icons.block), - text: AppLocalizations.of(context)!.blocked, - ), ] ) ) @@ -157,16 +151,6 @@ class _ClientsWidgetState extends State with TickerProviderStateM fetchClients: fetchClients, ) ), - RefreshIndicator( - onRefresh: fetchClients, - child: BlockedList( - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [], - fetchClients: fetchClients, - ) - ), ] ) ), diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart index 6e12df7..88a0cac 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/clients/block_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/client_modal.dart'; import 'package:adguard_home_manager/models/clients.dart'; @@ -61,7 +60,6 @@ class ClientsFab extends StatelessWidget { ); } else if (result['result'] == 'error' && result['message'] == 'client_another_list') { - appConfigProvider.addLog(result['log']); appConfigProvider.setShowingSnackbar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -114,17 +112,6 @@ class ClientsFab extends StatelessWidget { } } - void openBlockClient() { - showModalBottomSheet( - context: context, - builder: (ctx) => BlockClientModal( - onConfirm: confirmRemoveDomain - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - void openAddClient() { showModalBottomSheet( context: context, @@ -136,20 +123,9 @@ class ClientsFab extends StatelessWidget { ); } - if (tab == 1) { - return FloatingActionButton( - onPressed: () => openAddClient(), - child: const Icon(Icons.add), - ); - } - else if (tab == 2) { - return FloatingActionButton( - onPressed: () => openBlockClient(), - child: const Icon(Icons.add), - ); - } - else { - return const SizedBox(); - } + return FloatingActionButton( + onPressed: () => openAddClient(), + child: const Icon(Icons.add), + ); } } \ No newline at end of file diff --git a/lib/screens/settings/access_settings.dart b/lib/screens/settings/access_settings.dart new file mode 100644 index 0000000..40336c6 --- /dev/null +++ b/lib/screens/settings/access_settings.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/settings/clients_list.dart'; + +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class AccessSettings extends StatelessWidget { + const AccessSettings({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + return AccessSettingsWidget( + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + ); + } +} + +class AccessSettingsWidget extends StatefulWidget { + final ServersProvider serversProvider; + final AppConfigProvider appConfigProvider; + + const AccessSettingsWidget({ + Key? key, + required this.serversProvider, + required this.appConfigProvider, + }) : super(key: key); + + @override + State createState() => _AccessSettingsWidgetState(); +} + +class _AccessSettingsWidgetState extends State with TickerProviderStateMixin { + final ScrollController scrollController = ScrollController(); + late TabController tabController; + + Future fetchClients() async { + widget.serversProvider.setClientsLoadStatus(0, false); + final result = await getClients(widget.serversProvider.selectedServer!); + if (mounted) { + if (result['result'] == 'success') { + widget.serversProvider.setClientsData(result['data']); + widget.serversProvider.setClientsLoadStatus(1, true); + } + else { + widget.appConfigProvider.addLog(result['log']); + widget.serversProvider.setClientsLoadStatus(2, true); + } + } + } + + + @override + void initState() { + fetchClients(); + super.initState(); + tabController = TabController( + initialIndex: 0, + length: 3, + vsync: this, + ); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + + return Scaffold( + body: DefaultTabController( + length: 3, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverAppBar( + title: Text(AppLocalizations.of(context)!.accessSettings), + pinned: true, + floating: true, + forceElevated: innerBoxIsScrolled, + bottom: TabBar( + controller: tabController, + tabs: [ + Tab( + icon: const Icon(Icons.check), + text: AppLocalizations.of(context)!.allowedClients, + ), + Tab( + icon: const Icon(Icons.block), + text: AppLocalizations.of(context)!.disallowedClients, + ), + Tab( + icon: const Icon(Icons.link_rounded), + text: AppLocalizations.of(context)!.disallowedDomains, + ), + ] + ) + ) + ]; + }), + body: Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border( + top: BorderSide( + color: Theme.of(context).brightness == Brightness.light + ? const Color.fromRGBO(220, 220, 220, 1) + : const Color.fromRGBO(50, 50, 50, 1) + ) + ) + ), + child: TabBarView( + controller: tabController, + children: [ + ClientsList( + type: 'allowed', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == 1 + ? serversProvider.clients.data!.clientsAllowedBlocked!.allowedClients : [], + fetchClients: fetchClients + ), + ClientsList( + type: 'disallowed', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == 1 + ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [], + fetchClients: fetchClients + ), + ClientsList( + type: 'domains', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == 1 + ? serversProvider.clients.data!.clientsAllowedBlocked!.blockedHosts : [], + fetchClients: fetchClients + ), + ] + ) + ), + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/block_client_modal.dart b/lib/screens/settings/add_client_modal.dart similarity index 58% rename from lib/screens/clients/block_client_modal.dart rename to lib/screens/settings/add_client_modal.dart index 0269557..dfad7d1 100644 --- a/lib/screens/clients/block_client_modal.dart +++ b/lib/screens/settings/add_client_modal.dart @@ -1,46 +1,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class BlockClientModal extends StatefulWidget { - final void Function(String) onConfirm; +class AddClientModal extends StatefulWidget { + final String type; + final void Function(String, String) onConfirm; - const BlockClientModal({ + const AddClientModal({ Key? key, + required this.type, required this.onConfirm }) : super(key: key); @override - State createState() => _BlockClientModalState(); + State createState() => _AddClientModalState(); } -class _BlockClientModalState extends State { - TextEditingController ipController = TextEditingController(); - String? ipError; +class _AddClientModalState extends State { + TextEditingController fieldController = TextEditingController(); - void validateIp(String value) { - RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); - if (ipAddress.hasMatch(value) == true) { - setState(() => ipError = null); + bool validData = false; + + void checkValidValues() { + if (fieldController.text != '') { + setState(() => validData = true); } else { - setState(() => ipError = AppLocalizations.of(context)!.ipNotValid); - } - } - - bool checkValidValues() { - if ( - ipController.text != '' && - ipError == null - ) { - return true; - } - else { - return false; + setState(() => validData = false); } } @override Widget build(BuildContext context) { + IconData icon() { + switch (widget.type) { + case 'allowed': + return Icons.check; + + case 'disallowed': + return Icons.block; + + case 'domains': + return Icons.link_rounded; + + default: + return Icons.check; + } + } + + String title() { + switch (widget.type) { + case 'allowed': + return AppLocalizations.of(context)!.allowClient; + + case 'disallowed': + return AppLocalizations.of(context)!.disallowClient; + + case 'domains': + return AppLocalizations.of(context)!.disallowedDomains; + + default: + return ""; + } + } + return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( @@ -55,30 +77,33 @@ class _BlockClientModalState extends State { ), child: Column( children: [ - const Icon( - Icons.block, + Icon( + icon(), size: 26, ), const SizedBox(height: 20), Text( - AppLocalizations.of(context)!.blockClient, + title(), style: const TextStyle( fontSize: 24 ), ), const SizedBox(height: 30), TextFormField( - controller: ipController, - onChanged: validateIp, + controller: fieldController, + onChanged: (_) => checkValidValues(), decoration: InputDecoration( prefixIcon: const Icon(Icons.link_rounded), - errorText: ipError, border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10) ) ), - labelText: AppLocalizations.of(context)!.ipAddress, + helperText: widget.type == 'allowed' || widget.type == 'blocked' + ? AppLocalizations.of(context)!.addClientFieldDescription : null, + labelText: widget.type == 'allowed' || widget.type == 'blocked' + ? AppLocalizations.of(context)!.clientIdentifier + : AppLocalizations.of(context)!.domain, ), ), Expanded( @@ -96,16 +121,16 @@ class _BlockClientModalState extends State { ), const SizedBox(width: 20), TextButton( - onPressed: checkValidValues() == true + onPressed: validData == true ? () { Navigator.pop(context); - widget.onConfirm(ipController.text); + widget.onConfirm(fieldController.text, widget.type); } : null, child: Text( AppLocalizations.of(context)!.confirm, style: TextStyle( - color: checkValidValues() == true + color: validData == true ? Theme.of(context).primaryColor : Colors.grey ), diff --git a/lib/screens/settings/clients_list.dart b/lib/screens/settings/clients_list.dart new file mode 100644 index 0000000..2a59963 --- /dev/null +++ b/lib/screens/settings/clients_list.dart @@ -0,0 +1,355 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/settings/add_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; + +class ClientsList extends StatefulWidget { + final String type; + final ScrollController scrollController; + final int loadStatus; + final List data; + final Future Function() fetchClients; + + const ClientsList({ + Key? key, + required this.type, + required this.scrollController, + required this.loadStatus, + required this.data, + required this.fetchClients + }) : super(key: key); + + @override + State createState() => _ClientsListState(); +} + +class _ClientsListState extends State { + late bool isVisible; + + @override + initState(){ + super.initState(); + + isVisible = true; + widget.scrollController.addListener(() { + if (widget.scrollController.position.userScrollDirection == ScrollDirection.reverse) { + if (mounted && isVisible == true) { + setState(() => isVisible = false); + } + } + else { + if (widget.scrollController.position.userScrollDirection == ScrollDirection.forward) { + if (mounted && isVisible == false) { + setState(() => isVisible = true); + } + } + } + }); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void confirmRemoveItem(String client, String type) async { + Map> body = { + "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], + }; + + if (type == 'allowed') { + body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); + } + else if (type == 'disallowed') { + body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); + } + else if (type == 'domains') { + body['blocked_hosts'] = body['blocked_hosts']!.where((c) => c != client).toList(); + } + + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.removingClient); + + final result = await requestAllowedBlockedClientsHosts(serversProvider.selectedServer!, body); + + processModal.close(); + + if (result['result'] == 'success') { + serversProvider.setAllowedDisallowedClientsBlockedDomains( + ClientsAllowedBlocked( + allowedClients: body['allowed_clients'] ?? [], + disallowedClients: body['disallowed_clients'] ?? [], + blockedHosts: body['blocked_hosts'] ?? [], + ) + ); + } + else if (result['result'] == 'error' && result['message'] == 'client_another_list') { + appConfigProvider.setShowingSnackbar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.clientAnotherList), + backgroundColor: Colors.red, + ) + ); + } + else { + appConfigProvider.addLog(result['log']); + appConfigProvider.setShowingSnackbar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.clientNotRemoved), + backgroundColor: Colors.red, + ) + ); + } + } + + void confirmAddItem(String item, String type) async { + Map> body = { + "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], + }; + + if (type == 'allowed') { + body['allowed_clients']!.add(item); + } + else if (type == 'disallowed') { + body['disallowed_clients']!.add(item); + } + else if (type == 'domains') { + body['blocked_hosts']!.add(item); + } + + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.removingClient); + + final result = await requestAllowedBlockedClientsHosts(serversProvider.selectedServer!, body); + + processModal.close(); + + if (result['result'] == 'success') { + serversProvider.setAllowedDisallowedClientsBlockedDomains( + ClientsAllowedBlocked( + allowedClients: body['allowed_clients'] ?? [], + disallowedClients: body['disallowed_clients'] ?? [], + blockedHosts: body['blocked_hosts'] ?? [], + ) + ); + } + else if (result['result'] == 'error' && result['message'] == 'client_another_list') { + appConfigProvider.setShowingSnackbar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.clientAnotherList), + backgroundColor: Colors.red, + ) + ); + } + else { + appConfigProvider.addLog(result['log']); + appConfigProvider.setShowingSnackbar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + type == 'allowed' || type == 'blocked' + ? AppLocalizations.of(context)!.clientNotRemoved + : AppLocalizations.of(context)!.domainNotAdded + ), + backgroundColor: Colors.red, + ) + ); + } + } + + String description() { + switch (widget.type) { + case 'allowed': + return AppLocalizations.of(context)!.allowedClientsDescription; + + case 'disallowed': + return AppLocalizations.of(context)!.blockedClientsDescription; + + case 'domains': + return AppLocalizations.of(context)!.disallowedDomainsDescription; + + default: + return ""; + } + } + + String noItems() { + switch (widget.type) { + case 'allowed': + return AppLocalizations.of(context)!.noAllowedClients; + + case 'disallowed': + return AppLocalizations.of(context)!.noBlockedClients; + + case 'domains': + return AppLocalizations.of(context)!.noDisallowedDomains; + + default: + return ""; + } + } + + switch (widget.loadStatus) { + case 0: + return SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingClients, + style: const TextStyle( + fontSize: 22, + color: Colors.grey, + ), + ) + ], + ), + ); + + case 1: + return Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + const Icon(Icons.info_rounded), + const SizedBox(width: 20), + SizedBox( + width: MediaQuery.of(context).size.width-112, + child: Text(description()), + ) + ], + ), + ), + ), + ), + if (widget.data.isNotEmpty) Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: widget.data.length, + itemBuilder: (context, index) => ListTile( + title: Text(widget.data[index]), + trailing: IconButton( + onPressed: () => { + showDialog( + context: context, + builder: (context) => RemoveClientModal( + onConfirm: () => confirmRemoveItem(widget.data[index], widget.type), + ) + ) + }, + icon: const Icon(Icons.delete_rounded) + ), + ) + ), + ), + if (widget.data.isEmpty) Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + noItems(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + color: Colors.grey + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: widget.fetchClients, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + ], + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: FloatingActionButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => AddClientModal( + type: widget.type, + onConfirm: confirmAddItem + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + }, + child: const Icon(Icons.add), + ), + ) + ] + ); + + case 2: + return SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.clientsNotLoaded, + style: const TextStyle( + fontSize: 22, + color: Colors.grey, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + + } +} \ No newline at end of file diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 3cd0b00..09e5c54 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -7,6 +7,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/theme_modal.dart'; import 'package:adguard_home_manager/screens/settings/custom_list_tile.dart'; import 'package:adguard_home_manager/screens/settings/server_info.dart'; +import 'package:adguard_home_manager/screens/settings/access_settings.dart'; import 'package:adguard_home_manager/screens/settings/section_label.dart'; import 'package:adguard_home_manager/screens/settings/appbar.dart'; import 'package:adguard_home_manager/screens/servers/servers.dart'; @@ -85,6 +86,18 @@ class Settings extends StatelessWidget { body: ListView( children: [ SectionLabel(label: AppLocalizations.of(context)!.serverSettings), + CustomListTile( + leadingIcon: Icons.lock_rounded, + label: AppLocalizations.of(context)!.accessSettings, + description: AppLocalizations.of(context)!.accessSettingsDescription, + onTap: () => { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AccessSettings() + ) + ) + }, + ), CustomListTile( leadingIcon: Icons.info_rounded, label: AppLocalizations.of(context)!.serverInformation,