diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 491c21b..673b4c7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -291,5 +291,16 @@ "domainNotValid": "Domain not valid", "check": "Check", "checkingHost": "Checking host...", - "errorCheckingHost": "Host couldn't be checked" + "errorCheckingHost": "Host couldn't be checked", + "block": "Block", + "unblock": "Unblock", + "custom": "Custom", + "addImportant": "Add $important", + "howCreateRules": "How to create custom rules", + "examples": "Examples", + "example1": "Block access to example.org and all its subdomains.", + "example2": "Unblocks access to example.org and all its subdomains.", + "example3": "Adds a comment.", + "example4": "Block access to domains matching the specified regular expression.", + "moreInformation": "More information" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 86c1067..28c9693 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -291,5 +291,16 @@ "domainNotValid": "Dominio no válido", "check": "Comprobar", "checkingHost": "Comprobando host...", - "errorCheckingHost": "No se pudo comprobar el host" + "errorCheckingHost": "No se pudo comprobar el host", + "block": "Bloquear", + "unblock": "Desbloquear", + "custom": "Personalizado", + "addImportant": "Añadir $important", + "howCreateRules": "Cómo crear reglas personalizadas", + "examples": "Ejemplos", + "example1": "Bloquea el acceso al dominio ejemplo.org y a todos sus subdominios.", + "example2": "Desbloquea el acceso al dominio ejemplo.org y a todos sus subdominios.", + "example3": "Añade un comentario.", + "example4": "Bloquea el acceso a los dominios que coincidan con la expresión regular especificada.", + "moreInformation": "Más información" } \ No newline at end of file diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart index 64b984b..06d002c 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -28,59 +28,6 @@ class ClientsFab extends StatelessWidget { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - void confirmRemoveDomain(String ip) async { - Map> body = {}; - - 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)!.addingClient); - - 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'] ?? [], - ) - ); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.clientAddedSuccessfully, - color: Colors.green - ); - } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.clientAnotherList, - color: Colors.red - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.clientNotAdded, - color: Colors.red - ); - } - } - void confirmAddClient(Client client) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.addingClient); diff --git a/lib/screens/filters/add_custom_rule.dart b/lib/screens/filters/add_custom_rule.dart index cfba9c1..578c28a 100644 --- a/lib/screens/filters/add_custom_rule.dart +++ b/lib/screens/filters/add_custom_rule.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/widgets/custom_radio_toggle.dart'; + class AddCustomRule extends StatefulWidget { + final ScrollController scrollController; final void Function(String) onConfirm; const AddCustomRule({ Key? key, + required this.scrollController, required this.onConfirm }) : super(key: key); @@ -14,26 +18,59 @@ class AddCustomRule extends StatefulWidget { } class _AddCustomRuleState extends State { - TextEditingController ruleController = TextEditingController(); + final TextEditingController domainController = TextEditingController(); + String? domainError; bool validValues = false; + String preset = "block"; + + bool addImportant = false; + void checkValidValues() { - if (ruleController.text != '') { + if (domainController.text != '') { setState(() => validValues = true); } else { setState(() => validValues = false); } } + + void validateDomain(String value) { + final domainRegex = RegExp(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$'); + if (domainRegex.hasMatch(value)) { + setState(() => domainError = null); + } + else { + setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); + } + } + + String buildRule() { + String rule = ""; + + if (preset == 'block') { + rule = "||${domainController.text.trim()}^"; + } + else if (preset == 'unblock') { + rule = "@@||${domainController.text.trim()}^"; + } + else { + rule = domainController.text.trim(); + } + + if (addImportant == true) { + rule = "$rule\$important"; + } + + return rule; + } @override Widget build(BuildContext context) { return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( - height: 300, - padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: Theme.of(context).dialogBackgroundColor, borderRadius: const BorderRadius.only( @@ -43,64 +80,300 @@ class _AddCustomRuleState extends State { ), child: Column( children: [ - const Icon( - Icons.shield_rounded, - size: 26, - ), - const SizedBox(height: 20), - Text( - AppLocalizations.of(context)!.addCustomRule, - style: const TextStyle( - fontSize: 24 - ), - ), - const SizedBox(height: 30), - TextFormField( - controller: ruleController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.rule), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.rule, - ), - ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, + child: ListView( + controller: widget.scrollController, children: [ - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) + const Padding( + padding: EdgeInsets.only(top: 28), + child: Icon( + Icons.shield_rounded, + size: 26, + ), + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.addCustomRule, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24 + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 ), - const SizedBox(width: 20), - TextButton( - onPressed: validValues == true - ? () { - Navigator.pop(context); - widget.onConfirm(ruleController.text); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validValues == true - ? Theme.of(context).primaryColor - : Colors.grey - ), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).primaryColor ) ), - ], + child: Text( + buildRule(), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500 + ), + ) + ), + ], + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: TextFormField( + controller: domainController, + onChanged: validateDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, + ), ), ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomRadioToggle( + groupSelected: preset, + value: 'block', + label: AppLocalizations.of(context)!.block, + onTap: (value) => setState(() => preset = value) + ), + CustomRadioToggle( + groupSelected: preset, + value: 'unblock', + label: AppLocalizations.of(context)!.unblock, + onTap: (value) => setState(() => preset = value) + ), + CustomRadioToggle( + groupSelected: preset, + value: 'custom', + label: AppLocalizations.of(context)!.custom, + onTap: (value) => setState(() => preset = value) + ), + ], + ), + const SizedBox(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => addImportant = !addImportant), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.addImportant, + style: const TextStyle( + fontSize: 16 + ), + ), + ), + Switch( + value: addImportant, + onChanged: (value) => setState(() => addImportant = value), + activeColor: Theme.of(context).primaryColor, + ) + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.info), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.examples, + style: const TextStyle( + fontSize: 18 + ), + ) + ], + ), + const SizedBox(height: 20), + SizedBox( + width: double.maxFinite, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example1, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 20), + Text( + "@@||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example2, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 20), + Text( + "! Here goes a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor + ), + ), + Text( + "# Also a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example3, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 20), + Text( + "/REGEX/", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example4, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor + ), + ), + ], + ), + ) + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => {}, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.moreInformation, + style: const TextStyle( + fontSize: 16 + ), + ), + ), + const Padding( + padding: EdgeInsets.only(right: 15), + child: Icon(Icons.open_in_new), + ) + ], + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 28, + right: 28, + top: 20, + bottom: 28 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: validValues == true + ? () { + Navigator.pop(context); + widget.onConfirm(domainController.text); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validValues == true + ? Theme.of(context).primaryColor + : Colors.grey + ), + ) + ), ], ), ) diff --git a/lib/screens/filters/fab.dart b/lib/screens/filters/fab.dart index aec4349..2c26a70 100644 --- a/lib/screens/filters/fab.dart +++ b/lib/screens/filters/fab.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:bottom_sheet/bottom_sheet.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/filters/add_custom_rule.dart'; @@ -63,13 +64,19 @@ class FiltersFab extends StatelessWidget { } void openAddCustomRule() { - showModalBottomSheet( + showFlexibleBottomSheet( + minHeight: 0.7, + initHeight: 0.7, + maxHeight: 0.95, + isCollapsible: true, + duration: const Duration(milliseconds: 250), + anchors: [0.7, 0.95], context: context, - builder: (ctx) => AddCustomRule( + builder: (ctx, controller, offset) => AddCustomRule( + scrollController: controller, onConfirm: confirmAddRule ), - isScrollControlled: true, - backgroundColor: Colors.transparent + bottomSheetColor: Colors.transparent ); }