diff --git a/lib/config/app_screens.dart b/lib/config/app_screens.dart index 8987d6c..a73d5ff 100644 --- a/lib/config/app_screens.dart +++ b/lib/config/app_screens.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:adguard_home_manager/screens/connect/fab.dart'; import 'package:adguard_home_manager/screens/home/appbar.dart'; import 'package:adguard_home_manager/screens/connect/appbar.dart'; +import 'package:adguard_home_manager/screens/clients/fab.dart'; import 'package:adguard_home_manager/screens/connect/connect.dart'; import 'package:adguard_home_manager/screens/home/home.dart'; import 'package:adguard_home_manager/screens/clients/clients.dart'; @@ -40,6 +41,7 @@ List screensServerConnected = [ name: "clients", icon: Icons.devices, body: Clients(), + fab: ClientsFab() ), const AppScreen( name: "settings", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8c909eb..89c2f35 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -87,5 +87,10 @@ "removeClientMessage": "Are you sure you want to remove this client from the list?", "confirm": "Confirm", "removingClient": "Removing client...", - "clientNotRemoved": "Client could not be removed from the list" + "clientNotRemoved": "Client could not be removed from the list", + "addClient": "Add client", + "list": "List", + "ipAddress": "IP address", + "ipNotValid": "IP address not valid", + "clientAddedSuccessfully": "Client added to the list successfully" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 30b9403..7b6e27b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -87,5 +87,10 @@ "removeClientMessage": "Estás seguro que deseas eliminar este cliente de la lista?", "confirm": "Confirmar", "removingClient": "Eliminando cliente...", - "clientNotRemoved": "El cliente no pudo ser eliminado de la lista" + "clientNotRemoved": "El cliente no pudo ser eliminado de la lista", + "addClient": "Agregar cliente", + "list": "Lista", + "ipAddress": "Dirección IP", + "ipNotValid": "Dirección IP no válida", + "clientAddedSuccessfully": "Cliente añadido a la lista satisfactoriamente" } \ No newline at end of file diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 2700ef1..b1726c9 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -13,6 +13,8 @@ class AppConfigProvider with ChangeNotifier { int _selectedTheme = 0; + int _selectedClientsTab = 0; + PackageInfo? get getAppInfo { return _appInfo; } @@ -47,6 +49,9 @@ class AppConfigProvider with ChangeNotifier { return _selectedTheme; } + int get selectedClientsTab { + return _selectedClientsTab; + } void setDbInstance(Database db) { _dbInstance = db; @@ -64,6 +69,11 @@ class AppConfigProvider with ChangeNotifier { _iosDeviceInfo = deviceInfo; } + void setSelectedClientsTab(int tab) { + _selectedClientsTab = tab; + notifyListeners(); + } + Future setSelectedTheme(int value) async { final updated = await _updateThemeDb(value); if (updated == true) { diff --git a/lib/screens/clients/add_client_modal.dart b/lib/screens/clients/add_client_modal.dart new file mode 100644 index 0000000..2a377e9 --- /dev/null +++ b/lib/screens/clients/add_client_modal.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AddClientModal extends StatefulWidget { + final String list; + final void Function(String, String) onConfirm; + + const AddClientModal({ + Key? key, + required this.list, + required this.onConfirm + }) : super(key: key); + + @override + State createState() => _AddClientModalState(); +} + +class _AddClientModalState extends State { + String list = ''; + + TextEditingController ipController = TextEditingController(); + String? ipError; + + @override + void initState() { + list = widget.list; + super.initState(); + } + + 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); + } + else { + setState(() => ipError = AppLocalizations.of(context)!.ipNotValid); + } + } + + bool checkValidValues() { + if ( + (list == 'allowed' || + list == 'blocked') && + ipController.text != '' && + ipError == null + ) { + return true; + } + else { + return false; + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + const Icon( + Icons.add, + size: 26, + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.addClient, + ) + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15) + ), + color: Colors.transparent, + child: InkWell( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15) + ), + onTap: () => setState(() => list = 'allowed'), + child: AnimatedContainer( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15) + ), + border: Border.all( + color: Theme.of(context).primaryColor + ), + color: list == 'allowed' + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColor.withOpacity(0.05) + ), + child: Text( + AppLocalizations.of(context)!.allowed, + style: TextStyle( + color: list == 'allowed' + ? Colors.white + : null + ), + ), + ), + ), + ), + Material( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(15), + bottomRight: Radius.circular(15) + ), + color: Colors.transparent, + child: InkWell( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(15), + bottomRight: Radius.circular(15) + ), + onTap: () => setState(() => list = 'blocked'), + child: AnimatedContainer( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(15), + bottomRight: Radius.circular(15) + ), + border: Border.all( + color: Theme.of(context).primaryColor + ), + color: list == 'blocked' + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColor.withOpacity(0.05) + ), + child: Text( + AppLocalizations.of(context)!.blocked, + style: TextStyle( + color: list == 'blocked' + ? Colors.white + : null + ), + ), + ), + ), + ) + ], + ), + const SizedBox(height: 30), + TextFormField( + controller: ipController, + onChanged: validateIp, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + errorText: ipError, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.ipAddress, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + TextButton( + onPressed: checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm(list, ipController.text); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: checkValidValues() == true + ? Theme.of(context).primaryColor + : Colors.grey + ), + ) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index caf68ac..d8241c9 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -5,6 +5,7 @@ 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_allowed_list.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/clients.dart'; @@ -16,11 +17,13 @@ class Clients extends StatelessWidget { @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); return ClientsWidget( server: serversProvider.selectedServer!, setLoadingStatus: serversProvider.setClientsLoadStatus, setClientsData: serversProvider.setClientsData, + setSelectedClientsTab: appConfigProvider.setSelectedClientsTab, ); } } @@ -29,19 +32,23 @@ class ClientsWidget extends StatefulWidget { final Server server; final void Function(int, bool) setLoadingStatus; final void Function(ClientsData) setClientsData; + final void Function(int) setSelectedClientsTab; const ClientsWidget({ Key? key, required this.server, required this.setLoadingStatus, required this.setClientsData, + required this.setSelectedClientsTab }) : super(key: key); @override State createState() => _ClientsWidgetState(); } -class _ClientsWidgetState extends State { +class _ClientsWidgetState extends State with TickerProviderStateMixin { + late TabController tabController; + void fetchClients() async { widget.setLoadingStatus(0, false); final result = await getClients(widget.server); @@ -58,6 +65,12 @@ class _ClientsWidgetState extends State { void initState() { fetchClients(); super.initState(); + tabController = TabController( + initialIndex: 0, + length: 3, + vsync: this, + ); + tabController.addListener(() => widget.setSelectedClientsTab(tabController.index)); } List generateClientsList(List clients, List ips) { @@ -103,6 +116,7 @@ class _ClientsWidgetState extends State { floating: true, forceElevated: innerBoxIsScrolled, bottom: TabBar( + controller: tabController, tabs: [ Tab( icon: const Icon(Icons.devices), @@ -132,6 +146,7 @@ class _ClientsWidgetState extends State { ) ), child: TabBarView( + controller: tabController, children: [ ClientsList( data: serversProvider.clients.data!.autoClientsData, diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart new file mode 100644 index 0000000..57e7cda --- /dev/null +++ b/lib/screens/clients/fab.dart @@ -0,0 +1,101 @@ +// ignore_for_file: use_build_context_synchronously + +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/add_client_modal.dart'; + +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class ClientsFab extends StatelessWidget { + const ClientsFab({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void confirmRemoveDomain(String list, String ip) async { + Map> body = {}; + + if (list == 'allowed') { + final List clients = [...serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], ip]; + body = { + "allowed_clients": clients, + "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], + }; + } + else if (list == 'blocked') { + final List clients = [...serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], ip]; + body = { + "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": clients, + "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'] ?? [], + ) + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.clientAddedSuccessfully), + backgroundColor: Colors.green, + ) + ); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.clientNotRemoved), + backgroundColor: Colors.red, + ) + ); + } + } + + void openAddClient(String list) { + showDialog( + context: context, + builder: (ctx) => AddClientModal( + list: list, + onConfirm: confirmRemoveDomain + ) + ); + } + + if (appConfigProvider.selectedClientsTab == 1) { + return FloatingActionButton( + onPressed: () => openAddClient('allowed'), + child: const Icon(Icons.add), + ); + } + else if (appConfigProvider.selectedClientsTab == 2) { + return FloatingActionButton( + onPressed: () => openAddClient('blocked'), + child: const Icon(Icons.add), + ); + } + else { + return const SizedBox(); + } + } +} \ No newline at end of file diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index 25237b0..3f95096 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -1,6 +1,8 @@ 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/app_config_provider.dart'; import 'package:adguard_home_manager/models/app_screen.dart'; class BottomNavBar extends StatelessWidget { @@ -17,6 +19,8 @@ class BottomNavBar extends StatelessWidget { @override Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + String translatedName(String key) { switch (key) { case 'home': @@ -42,7 +46,13 @@ class BottomNavBar extends StatelessWidget { icon: Icon(screen.icon), label: translatedName(screen.name) )).toList(), - onDestinationSelected: onSelect, + onDestinationSelected: (value) { + // Reset clients tab to 0 when changing screen + if (value != 1) { + appConfigProvider.setSelectedClientsTab(0); + } + onSelect(value); + }, ); } } \ No newline at end of file