diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bec25f4..df82cec 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -207,5 +207,14 @@ "customRules": "Custom rules", "enabledRules": "Enabled rules", "enabled": "Enabled", - "disabled": "Disabled" + "disabled": "Disabled", + "rule": "Rule", + "addCustomRule": "Add custom rule", + "removeCustomRule": "Remove custom rule", + "removeCustomRuleMessage": "Are you sure you want to remove this custom rule?", + "updatingRules": "Updating custom rules...", + "ruleRemovedSuccessfully": "Rule removed successfully", + "ruleNotRemoved": "Couldn't remove the rule", + "ruleAddedSuccessfully": "Rule added successfully", + "ruleNotAdded": "Couldn't add the rule," } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3a55a11..0c0453b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -207,5 +207,14 @@ "customRules": "Reglas personalizadas", "enabledRules": "Reglas activas", "enabled": "Activada", - "disabled": "Desactivada" + "disabled": "Desactivada", + "rule": "Regla", + "addCustomRule": "Añadir regla personalizada", + "removeCustomRule": "Eliminar regla personalizada", + "removeCustomRuleMessage": "¿Estás seguro que deseas eliminar esta regla personalizada?", + "updatingRules": "Updating custom rules...", + "ruleRemovedSuccessfully": "Regla eliminada correctamente", + "ruleNotRemoved": "No se ha podido eliminar la regla", + "ruleAddedSuccessfully": "Regla añadida correctamente", + "ruleNotAdded": "No se ha podido añadir la regla" } \ No newline at end of file diff --git a/lib/models/filtering.dart b/lib/models/filtering.dart index be97734..d230b55 100644 --- a/lib/models/filtering.dart +++ b/lib/models/filtering.dart @@ -17,7 +17,7 @@ class Filtering { class FilteringData { final List filters; final List whitelistFilters; - final List userRules; + List userRules; final int interval; final bool enabled; diff --git a/lib/screens/filters/add_custom_rule.dart b/lib/screens/filters/add_custom_rule.dart new file mode 100644 index 0000000..cfba9c1 --- /dev/null +++ b/lib/screens/filters/add_custom_rule.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AddCustomRule extends StatefulWidget { + final void Function(String) onConfirm; + + const AddCustomRule({ + Key? key, + required this.onConfirm + }) : super(key: key); + + @override + State createState() => _AddCustomRuleState(); +} + +class _AddCustomRuleState extends State { + TextEditingController ruleController = TextEditingController(); + + bool validValues = false; + + void checkValidValues() { + if (ruleController.text != '') { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } + + @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( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + 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, + 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 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 + ), + ) + ), + ], + ), + ), + ], + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/custom_rules_list.dart b/lib/screens/filters/custom_rules_list.dart index 3330888..17ae622 100644 --- a/lib/screens/filters/custom_rules_list.dart +++ b/lib/screens/filters/custom_rules_list.dart @@ -1,4 +1,17 @@ +// 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/filters/fab.dart'; +import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; class CustomRulesList extends StatelessWidget { final List data; @@ -10,17 +23,69 @@ class CustomRulesList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: data.length, - itemBuilder: (context, index) => ListTile( - onLongPress: () => {}, - title: Text(data[index]), - trailing: IconButton( - onPressed: () => {}, - icon: const Icon(Icons.delete) + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void removeCustomRule(String rule) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.updatingRules); + + final List newRules = serversProvider.filtering.data!.userRules.where((r) => r != rule).toList(); + + final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); + + processModal.close(); + + if (result['result'] == 'success') { + FilteringData filteringData = serversProvider.filtering.data!; + filteringData.userRules = newRules; + serversProvider.setFilteringData(filteringData); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.ruleRemovedSuccessfully), + backgroundColor: Colors.green, + ) + ); + } + else { + appConfigProvider.addLog(result['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.ruleNotRemoved), + backgroundColor: Colors.red, + ) + ); + } + } + + void openRemoveCustomRuleModal(String rule) { + showDialog( + context: context, + builder: (context) => RemoveCustomRule( + onConfirm: () => removeCustomRule(rule), + ) + ); + } + + return Stack( + children: [ + ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: data.length, + itemBuilder: (context, index) => ListTile( + title: Text(data[index]), + trailing: IconButton( + onPressed: () => openRemoveCustomRuleModal(data[index]), + icon: const Icon(Icons.delete) + ), + ) ), - ) + const Positioned( + bottom: 20, + right: 20, + child: FiltersFab() + ) + ], ); } } \ No newline at end of file diff --git a/lib/screens/filters/fab.dart b/lib/screens/filters/fab.dart new file mode 100644 index 0000000..be32b92 --- /dev/null +++ b/lib/screens/filters/fab.dart @@ -0,0 +1,72 @@ +// 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/filters/add_custom_rule.dart'; + +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class FiltersFab extends StatelessWidget { + const FiltersFab({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void confirmAddRule(String rule) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.updatingRules); + + final List newRules = serversProvider.filtering.data!.userRules; + newRules.add(rule); + + final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); + + processModal.close(); + + if (result['result'] == 'success') { + FilteringData filteringData = serversProvider.filtering.data!; + filteringData.userRules = newRules; + serversProvider.setFilteringData(filteringData); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.ruleAddedSuccessfully), + backgroundColor: Colors.green, + ) + ); + } + else { + appConfigProvider.addLog(result['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.ruleNotAdded), + backgroundColor: Colors.red, + ) + ); + } + } + + void openAddClient() { + showModalBottomSheet( + context: context, + builder: (ctx) => AddCustomRule( + onConfirm: confirmAddRule + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } + + return FloatingActionButton( + onPressed: openAddClient, + child: const Icon(Icons.add), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/remove_custom_rule_modal.dart b/lib/screens/filters/remove_custom_rule_modal.dart new file mode 100644 index 0000000..de25f4b --- /dev/null +++ b/lib/screens/filters/remove_custom_rule_modal.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class RemoveCustomRule extends StatelessWidget { + final void Function() onConfirm; + + const RemoveCustomRule({ + Key? key, + required this.onConfirm + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + const Icon( + Icons.shield_rounded, + size: 26, + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.removeCustomRule, + textAlign: TextAlign.center, + ) + ], + ), + content: Text(AppLocalizations.of(context)!.removeCustomRuleMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + TextButton( + onPressed: () { + onConfirm(); + Navigator.pop(context); + }, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 74c64a6..79f0edd 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -676,7 +676,7 @@ Future postDeleteClient({ required Server server, required String name, }) async { - final result = await apiRequest( + final result = await apiRequest( urlPath: '/clients/delete', method: 'post', server: server, @@ -739,4 +739,38 @@ Future getFiltering({ else { return result; } +} + +Future setCustomRules({ + required Server server, + required List rules, +}) async { + final result = await apiRequest( + urlPath: '/filtering/set_rules', + method: 'post', + server: server, + body: {'rules': rules}, + type: 'set_custom_rules' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'set_custom_rules', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } } \ No newline at end of file