From 6a285bf213293c3e4b37948993c1a62cd6de42da Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sat, 8 Oct 2022 00:31:44 +0200 Subject: [PATCH] Added add filter lists --- lib/l10n/app_en.arb | 14 +- lib/l10n/app_es.arb | 14 +- lib/models/filtering.dart | 6 +- lib/screens/clients/blocked_list.dart | 37 +++-- lib/screens/filters/add_list_modal.dart | 143 ++++++++++++++++++ lib/screens/filters/custom_rules_list.dart | 77 ++++++++-- lib/screens/filters/fab.dart | 101 ++++++++++++- lib/screens/filters/filters.dart | 16 ++- lib/screens/filters/filters_list.dart | 160 +++++++++++++++------ lib/services/http_requests.dart | 37 +++++ 10 files changed, 522 insertions(+), 83 deletions(-) create mode 100644 lib/screens/filters/add_list_modal.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index df82cec..076bebe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -216,5 +216,17 @@ "ruleRemovedSuccessfully": "Rule removed successfully", "ruleNotRemoved": "Couldn't remove the rule", "ruleAddedSuccessfully": "Rule added successfully", - "ruleNotAdded": "Couldn't add the rule," + "ruleNotAdded": "Couldn't add the rule", + "noCustomFilters": "No custom filters", + "noBlackLists": "No blacklists", + "noWhiteLists": "No whitelists", + "addWhitelist": "Add whitelist", + "addBlacklist": "Add blacklist", + "urlNotValid": "URL is not valid", + "urlAbsolutePath": "URL or absolute path", + "addingList": "Adding list...", + "listAdded": "List added successfully. Items added:", + "listAlreadyAdded": "List already added", + "listUrlInvalid": "List URL invalid", + "listNotAdded": "List 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 0c0453b..119465a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -216,5 +216,17 @@ "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" + "ruleNotAdded": "No se ha podido añadir la regla", + "noCustomFilters": "No custom filters", + "noBlackLists": "No hay listas negras", + "noWhiteLists": "No hay listas blancas", + "addWhitelist": "Añadir lista blanca", + "addBlacklist": "Añadir lista negra", + "urlNotValid": "La URL no es válida", + "urlAbsolutePath": "URL o ruta absoluta", + "addingList": "Añadiendo lista...", + "listAdded": "Lista añadida correctamente. Items añadidos:", + "listAlreadyAdded": "La lista ya estaba añadida", + "listUrlInvalid": "URL de la lista no válida", + "listNotAdded": "La lista no se pudo añadir" } \ No newline at end of file diff --git a/lib/models/filtering.dart b/lib/models/filtering.dart index d230b55..f142bf5 100644 --- a/lib/models/filtering.dart +++ b/lib/models/filtering.dart @@ -30,9 +30,9 @@ class FilteringData { }); factory FilteringData.fromJson(Map json) => FilteringData( - filters: List.from(json["filters"].map((x) => Filter.fromJson(x))), - whitelistFilters: List.from(json["whitelist_filters"].map((x) => Filter.fromJson(x))), - userRules: List.from(json["user_rules"].map((x) => x)), + filters: json["filters"] != null ? List.from(json["filters"].map((x) => Filter.fromJson(x))) : [], + whitelistFilters: json["whitelist_filters"] != null ? List.from(json["whitelist_filters"].map((x) => Filter.fromJson(x))) : [], + userRules: json["user_rules"] != null ? List.from(json["user_rules"].map((x) => x)) : [], interval: json["interval"], enabled: json["enabled"], ); diff --git a/lib/screens/clients/blocked_list.dart b/lib/screens/clients/blocked_list.dart index 364de41..c929894 100644 --- a/lib/screens/clients/blocked_list.dart +++ b/lib/screens/clients/blocked_list.dart @@ -73,26 +73,23 @@ class BlockedList extends StatelessWidget { return Stack( children: [ - if (data.isNotEmpty) RefreshIndicator( - onRefresh: () async {}, - child: ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: data.length, - itemBuilder: (context, index) => ListTile( - title: Text(data[index]), - trailing: IconButton( - onPressed: () => { - showDialog( - context: context, - builder: (context) => RemoveDomainModal( - onConfirm: () => confirmRemoveDomain(data[index]), - ) + if (data.isNotEmpty) ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: data.length, + itemBuilder: (context, index) => ListTile( + title: Text(data[index]), + trailing: IconButton( + onPressed: () => { + showDialog( + context: context, + builder: (context) => RemoveDomainModal( + onConfirm: () => confirmRemoveDomain(data[index]), ) - }, - icon: const Icon(Icons.delete_rounded) - ), - ) - ), + ) + }, + icon: const Icon(Icons.delete_rounded) + ), + ) ), if (data.isEmpty) SizedBox( width: double.maxFinite, @@ -100,7 +97,7 @@ class BlockedList extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.noClientsList, + AppLocalizations.of(context)!.noCustomFilters, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, diff --git a/lib/screens/filters/add_list_modal.dart b/lib/screens/filters/add_list_modal.dart new file mode 100644 index 0000000..f68dde2 --- /dev/null +++ b/lib/screens/filters/add_list_modal.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AddListModal extends StatefulWidget { + final String type; + final void Function({required String name, required String url}) onConfirm; + + const AddListModal({ + Key? key, + required this.type, + required this.onConfirm, + }) : super(key: key); + + @override + State createState() => _AddListModalState(); +} + +class _AddListModalState extends State { + final TextEditingController nameController = TextEditingController(); + final TextEditingController urlController = TextEditingController(); + String? urlError; + + bool validData = false; + + void checkValidValues() { + if (nameController.text != '' && urlController.text != '') { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + } + + void validateUrl(String value) { + final urlRegex = RegExp(r'^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$'); + if (urlRegex.hasMatch(value)) { + setState(() => urlError = null); + } + else { + final pathRegex = RegExp(r'^(((\\|\/)[a-z0-9^&@{}\[\],$=!\-#\(\)%\.\+~_]+)*(\\|\/))([^\\\/:\*\<>\|]+\.[a-z0-9]+)$'); + if (pathRegex.hasMatch(value)) { + setState(() => urlError = null); + } + else { + setState(() => urlError = AppLocalizations.of(context)!.urlNotValid); + } + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + height: 408, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: Column( + children: [ + Icon( + widget.type == 'whitelist' + ? Icons.verified_user_rounded + : Icons.gpp_bad_rounded, + size: 26, + ), + const SizedBox(height: 20), + Text( + widget.type == 'whitelist' + ? AppLocalizations.of(context)!.addWhitelist + : AppLocalizations.of(context)!.addBlacklist, + style: const TextStyle( + fontSize: 24 + ), + ), + const SizedBox(height: 30), + TextFormField( + controller: nameController, + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.name, + ), + ), + const SizedBox(height: 30), + TextFormField( + controller: urlController, + onChanged: validateUrl, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: urlError, + labelText: AppLocalizations.of(context)!.urlAbsolutePath, + ), + ), + const SizedBox(height: 20), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onConfirm( + name: nameController.text, + url: urlController.text + ); + }, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ), + ], + ), + ) + ], + ), + ), + ); + } +} \ 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 17ae622..62595a7 100644 --- a/lib/screens/filters/custom_rules_list.dart +++ b/lib/screens/filters/custom_rules_list.dart @@ -1,6 +1,7 @@ // 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'; @@ -13,14 +14,46 @@ 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 { +class CustomRulesList extends StatefulWidget { + final ScrollController scrollController; final List data; + final void Function() fetchData; const CustomRulesList({ Key? key, - required this.data + required this.scrollController, + required this.data, + required this.fetchData }) : super(key: key); + @override + State createState() => _CustomRulesListState(); +} + +class _CustomRulesListState 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); @@ -69,21 +102,47 @@ class CustomRulesList extends StatelessWidget { return Stack( children: [ - ListView.builder( + if (widget.data.isNotEmpty) ListView.builder( padding: const EdgeInsets.only(top: 0), - itemCount: data.length, + itemCount: widget.data.length, itemBuilder: (context, index) => ListTile( - title: Text(data[index]), + title: Text(widget.data[index]), trailing: IconButton( - onPressed: () => openRemoveCustomRuleModal(data[index]), + onPressed: () => openRemoveCustomRuleModal(widget.data[index]), icon: const Icon(Icons.delete) ), ) ), - const Positioned( - bottom: 20, + if (widget.data.isEmpty) SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.noBlackLists, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + color: Colors.grey + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: widget.fetchData, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? 20 : -70, right: 20, - child: FiltersFab() + child: const FiltersFab( + type: 'custom_rule', + ) ) ], ); diff --git a/lib/screens/filters/fab.dart b/lib/screens/filters/fab.dart index be32b92..55aabe1 100644 --- a/lib/screens/filters/fab.dart +++ b/lib/screens/filters/fab.dart @@ -1,5 +1,6 @@ // ignore_for_file: use_build_context_synchronously +import 'package:adguard_home_manager/screens/filters/add_list_modal.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -13,7 +14,12 @@ 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); + final String type; + + const FiltersFab({ + Key? key, + required this.type, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -53,7 +59,7 @@ class FiltersFab extends StatelessWidget { } } - void openAddClient() { + void openAddCustomRule() { showModalBottomSheet( context: context, builder: (ctx) => AddCustomRule( @@ -64,8 +70,97 @@ class FiltersFab extends StatelessWidget { ); } + void confirmAddList({required String name, required String url}) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.addingList); + + final result1 = await addFilteringList(server: serversProvider.selectedServer!, data: { + 'name': name, + 'url': url, + 'whitelist': type == 'whitelist' ? true : false + }); + + if (result1['result'] == 'success') { + if (result1['data'].toString().contains("OK")) { + final result2 = await getFiltering(server: serversProvider.selectedServer!); + final items = result1['data'].toString().split(' ')[1]; + + if (result2['result'] == 'success') { + serversProvider.setFilteringData(result2['data']); + serversProvider.setFilteringLoadStatus(1, true); + } + else { + appConfigProvider.addLog(result2['log']); + serversProvider.setFilteringLoadStatus(2, true); + } + + processModal.close(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("${AppLocalizations.of(context)!.listAdded} $items."), + backgroundColor: Colors.green, + ) + ); + } + else { + processModal.close(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.listNotAdded), + backgroundColor: Colors.red, + ) + ); + } + } + else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains("Couldn't fetch filter from url")) { + processModal.close(); + appConfigProvider.addLog(result1['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.listUrlInvalid), + backgroundColor: Colors.red, + ) + ); + } + else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains('Filter URL already added')) { + processModal.close(); + appConfigProvider.addLog(result1['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.listAlreadyAdded), + backgroundColor: Colors.red, + ) + ); + } + else { + processModal.close(); + appConfigProvider.addLog(result1['log']); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.listNotAdded), + backgroundColor: Colors.red, + ) + ); + } + } + + void openAddWhitelistBlacklist() { + showModalBottomSheet( + context: context, + builder: (ctx) => AddListModal( + type: type, + onConfirm: confirmAddList + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } + return FloatingActionButton( - onPressed: openAddClient, + onPressed: type == 'blacklist' || type == 'whitelist' + ? () => openAddWhitelistBlacklist() + : () => openAddCustomRule(), child: const Icon(Icons.add), ); } diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index c1003fb..ebf2701 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -41,6 +41,7 @@ class FiltersWidget extends StatefulWidget { class _FiltersWidgetState extends State with TickerProviderStateMixin { late TabController tabController; + final ScrollController scrollController = ScrollController(); Future fetchFilters() async { widget.serversProvider.setFilteringLoadStatus(0, false); @@ -114,6 +115,7 @@ class _FiltersWidgetState extends State with TickerProviderStateM return DefaultTabController( length: 3, child: NestedScrollView( + controller: scrollController, headerSliverBuilder: ((context, innerBoxIsScrolled) { return [ SliverAppBar( @@ -158,19 +160,27 @@ class _FiltersWidgetState extends State with TickerProviderStateM RefreshIndicator( onRefresh: fetchFilters, child: FiltersList( - data: serversProvider.filtering.data!.whitelistFilters + scrollController: scrollController, + type: 'whitelist', + data: serversProvider.filtering.data!.whitelistFilters, + fetchData: fetchFilters, ) ), RefreshIndicator( onRefresh: fetchFilters, child: FiltersList( - data: serversProvider.filtering.data!.filters + scrollController: scrollController, + type: 'blacklist', + data: serversProvider.filtering.data!.filters, + fetchData: fetchFilters, ) ), RefreshIndicator( onRefresh: fetchFilters, child: CustomRulesList( - data: serversProvider.filtering.data!.userRules + scrollController: scrollController, + data: serversProvider.filtering.data!.userRules, + fetchData: fetchFilters, ) ), ], diff --git a/lib/screens/filters/filters_list.dart b/lib/screens/filters/filters_list.dart index 1c25c11..43a4b2e 100644 --- a/lib/screens/filters/filters_list.dart +++ b/lib/screens/filters/filters_list.dart @@ -1,72 +1,146 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/filters/fab.dart'; + import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/models/filtering.dart'; -class FiltersList extends StatelessWidget { +class FiltersList extends StatefulWidget { + final ScrollController scrollController; final List data; + final void Function() fetchData; + final String type; const FiltersList({ Key? key, - required this.data + required this.scrollController, + required this.data, + required this.fetchData, + required this.type, }) : super(key: key); + @override + State createState() => _FiltersListState(); +} + +class _FiltersListState 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) { - return ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: data.length, - itemBuilder: (context, index) => Material( - color: Colors.transparent, - child: InkWell( - onTap: () => {}, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( + children: [ + if (widget.data.isNotEmpty) ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: widget.data.length, + itemBuilder: (context, index) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () => {}, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: MediaQuery.of(context).size.width-130, - child: Text( - data[index].name, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width-130, + child: Text( + widget.data[index].name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16 + ), + ), ), - ), + const SizedBox(height: 5), + Text( + "${intFormat(widget.data[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + style: const TextStyle( + fontSize: 14, + color: Colors.grey + ), + ), + ], ), - const SizedBox(height: 5), Text( - "${intFormat(data[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", - style: const TextStyle( - fontSize: 14, - color: Colors.grey + widget.data[index].enabled == true + ? AppLocalizations.of(context)!.enabled + : AppLocalizations.of(context)!.disabled, + style: TextStyle( + color: widget.data[index].enabled == true + ? Colors.green + : Colors.red, + fontWeight: FontWeight.w500 ), - ), + ) ], ), - Text( - data[index].enabled == true - ? AppLocalizations.of(context)!.enabled - : AppLocalizations.of(context)!.disabled, - style: TextStyle( - color: data[index].enabled == true - ? Colors.green - : Colors.red, - fontWeight: FontWeight.w500 - ), - ) - ], + ), ), ), ), - ), + if (widget.data.isEmpty) if (widget.data.isEmpty) SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.type == 'blacklist' + ? AppLocalizations.of(context)!.noBlackLists + : AppLocalizations.of(context)!.noWhiteLists, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + color: Colors.grey + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: widget.fetchData, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? 20 : -70, + right: 20, + child: FiltersFab( + type: widget.type + ) + ) + ], ); } } \ No newline at end of file diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 79f0edd..6373205 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -773,4 +773,41 @@ Future setCustomRules({ else { return result; } + +} +Future addFilteringList({ + required Server server, + required Map data, +}) async { + final result = await apiRequest( + urlPath: '/filtering/add_url', + method: 'post', + server: server, + body: data, + type: 'add_filtering_url' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': result['body'] + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'add_filtering_url', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } } \ No newline at end of file