Improved add custom rule modal

This commit is contained in:
Juan Gilsanz Polo 2022-10-10 01:05:15 +02:00
parent 1035257361
commit b42218fac1
5 changed files with 363 additions and 114 deletions

View file

@ -291,5 +291,16 @@
"domainNotValid": "Domain not valid", "domainNotValid": "Domain not valid",
"check": "Check", "check": "Check",
"checkingHost": "Checking host...", "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"
} }

View file

@ -291,5 +291,16 @@
"domainNotValid": "Dominio no válido", "domainNotValid": "Dominio no válido",
"check": "Comprobar", "check": "Comprobar",
"checkingHost": "Comprobando host...", "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"
} }

View file

@ -28,59 +28,6 @@ class ClientsFab extends StatelessWidget {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
void confirmRemoveDomain(String ip) async {
Map<String, List<String>> body = {};
final List<String> 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 { void confirmAddClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient); processModal.open(AppLocalizations.of(context)!.addingClient);

View file

@ -1,11 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_radio_toggle.dart';
class AddCustomRule extends StatefulWidget { class AddCustomRule extends StatefulWidget {
final ScrollController scrollController;
final void Function(String) onConfirm; final void Function(String) onConfirm;
const AddCustomRule({ const AddCustomRule({
Key? key, Key? key,
required this.scrollController,
required this.onConfirm required this.onConfirm
}) : super(key: key); }) : super(key: key);
@ -14,12 +18,17 @@ class AddCustomRule extends StatefulWidget {
} }
class _AddCustomRuleState extends State<AddCustomRule> { class _AddCustomRuleState extends State<AddCustomRule> {
TextEditingController ruleController = TextEditingController(); final TextEditingController domainController = TextEditingController();
String? domainError;
bool validValues = false; bool validValues = false;
String preset = "block";
bool addImportant = false;
void checkValidValues() { void checkValidValues() {
if (ruleController.text != '') { if (domainController.text != '') {
setState(() => validValues = true); setState(() => validValues = true);
} }
else { else {
@ -27,13 +36,41 @@ class _AddCustomRuleState extends State<AddCustomRule> {
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: MediaQuery.of(context).viewInsets, padding: MediaQuery.of(context).viewInsets,
child: Container( child: Container(
height: 300,
padding: const EdgeInsets.all(28),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor, color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@ -43,37 +80,276 @@ class _AddCustomRuleState extends State<AddCustomRule> {
), ),
child: Column( child: Column(
children: [ children: [
const Icon( Expanded(
child: ListView(
controller: widget.scrollController,
children: [
const Padding(
padding: EdgeInsets.only(top: 28),
child: Icon(
Icons.shield_rounded, Icons.shield_rounded,
size: 26, size: 26,
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
AppLocalizations.of(context)!.addCustomRule, AppLocalizations.of(context)!.addCustomRule,
textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24 fontSize: 24
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
TextFormField( Row(
controller: ruleController, mainAxisAlignment: MainAxisAlignment.center,
onChanged: (_) => checkValidValues(), mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5
),
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( decoration: InputDecoration(
prefixIcon: const Icon(Icons.rule), prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(10) Radius.circular(10)
) )
), ),
labelText: AppLocalizations.of(context)!.rule, errorText: domainError,
labelText: AppLocalizations.of(context)!.domain,
), ),
), ),
Expanded( ),
child: Column( const SizedBox(height: 30),
mainAxisAlignment: MainAxisAlignment.end, 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: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 20), 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( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -86,7 +362,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
onPressed: validValues == true onPressed: validValues == true
? () { ? () {
Navigator.pop(context); Navigator.pop(context);
widget.onConfirm(ruleController.text); widget.onConfirm(domainController.text);
} }
: null, : null,
child: Text( child: Text(
@ -100,9 +376,6 @@ class _AddCustomRuleState extends State<AddCustomRule> {
), ),
], ],
), ),
),
],
),
) )
], ],
), ),

View file

@ -2,6 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/add_custom_rule.dart'; import 'package:adguard_home_manager/screens/filters/add_custom_rule.dart';
@ -63,13 +64,19 @@ class FiltersFab extends StatelessWidget {
} }
void openAddCustomRule() { 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, context: context,
builder: (ctx) => AddCustomRule( builder: (ctx, controller, offset) => AddCustomRule(
scrollController: controller,
onConfirm: confirmAddRule onConfirm: confirmAddRule
), ),
isScrollControlled: true, bottomSheetColor: Colors.transparent
backgroundColor: Colors.transparent
); );
} }