From c150d2ba230c3ba8cd532f9a2a92d392bbb1663b Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 4 Feb 2024 21:23:45 +0100 Subject: [PATCH] Added edit custom rules --- lib/l10n/app_en.arb | 7 +- lib/l10n/app_es.arb | 7 +- lib/providers/filtering_provider.dart | 17 + lib/screens/filters/add_button.dart | 84 +++- lib/screens/filters/custom_rules_list.dart | 29 +- .../filters/modals/add_custom_rule.dart | 416 ------------------ .../add_custom_rule/add_custom_rule.dart | 326 ++++++++++++++ .../add_custom_rule/custom_rule_docs.dart | 166 +++++++ .../add_custom_rule/edit_custom_rules.dart | 167 +++++++ lib/widgets/tab_content_list.dart | 6 +- 10 files changed, 789 insertions(+), 436 deletions(-) delete mode 100644 lib/screens/filters/modals/add_custom_rule.dart create mode 100644 lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart create mode 100644 lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart create mode 100644 lib/screens/filters/modals/add_custom_rule/edit_custom_rules.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 238b380..4786d6b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -764,5 +764,10 @@ "subnetPrefixLengthIpv6": "Subnet prefix length for IPv6", "rateLimitAllowlist": "Rate limit allowlist", "rateLimitAllowlistDescription": "IP addresses excluded from rate limiting", - "dnsOptions": "DNS options" + "dnsOptions": "DNS options", + "editor": "Editor", + "editCustomRules": "Edit custom rules", + "savingCustomRules": "Saving custom rules...", + "customRulesUpdatedSuccessfully": "Custom rules updated successfully", + "customRulesNotUpdated": "Custom rules could not be updated" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 718a093..d7ba7a0 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -764,5 +764,10 @@ "subnetPrefixLengthIpv6": "Longitud del prefijo de subred para IPv6", "rateLimitAllowlist": "Lista de permitidos de limitación de velocidad", "rateLimitAllowlistDescription": "Direcciones IP excluidas de la limitación de velocidad", - "dnsOptions": "Opciones de DNS" + "dnsOptions": "Opciones de DNS", + "editor": "Editor", + "editCustomRules": "Editar reglas personalizadas", + "savingCustomRules": "Guardando reglas personalizadas...", + "customRulesUpdatedSuccessfully": "Reglas personalizadas actualizadas correctamente", + "customRulesNotUpdated": "Las reglas personalizadas no pudieron ser actualizadas" } \ No newline at end of file diff --git a/lib/providers/filtering_provider.dart b/lib/providers/filtering_provider.dart index a264429..d43a1b5 100644 --- a/lib/providers/filtering_provider.dart +++ b/lib/providers/filtering_provider.dart @@ -289,6 +289,23 @@ class FilteringProvider with ChangeNotifier { } } + Future setCustomRules(List rules) async { + final newRules = rules.where((r) => r != " " && r != "").toList(); + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); + + if (result.successful == true) { + Filtering filteringData = filtering!; + filteringData.userRules = newRules; + _filtering = filteringData; + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + Future> addList({required String name, required String url, required String type}) async { final result1 = await _serversProvider!.apiClient2!.addFilteringList( data: { diff --git a/lib/screens/filters/add_button.dart b/lib/screens/filters/add_button.dart index dc652bc..bc5a22f 100644 --- a/lib/screens/filters/add_button.dart +++ b/lib/screens/filters/add_button.dart @@ -6,7 +6,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/screens/filters/modals/add_custom_rule.dart'; +import 'package:adguard_home_manager/screens/filters/modals/add_custom_rule/edit_custom_rules.dart'; +import 'package:adguard_home_manager/screens/filters/modals/add_custom_rule/add_custom_rule.dart'; import 'package:adguard_home_manager/screens/filters/details/add_list_modal.dart'; import 'package:adguard_home_manager/providers/filtering_provider.dart'; @@ -19,10 +20,10 @@ class AddFiltersButton extends StatelessWidget { final Widget Function(void Function()) widget; const AddFiltersButton({ - Key? key, + super.key, required this.type, required this.widget - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -56,6 +57,31 @@ class AddFiltersButton extends StatelessWidget { } } + void confirmEditCustomRules(List rules) async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingCustomRules); + + final result = await filteringProvider.setCustomRules(rules); + + processModal.close(); + + if (!context.mounted) return; + if (result == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.customRulesUpdatedSuccessfully, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.customRulesNotUpdated, + color: Colors.red + ); + } + } + void openAddCustomRule() { showGeneralDialog( context: context, @@ -83,6 +109,33 @@ class AddFiltersButton extends StatelessWidget { ); } + void openEditCustomRule() { + showGeneralDialog( + context: context, + barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => EditCustomRules( + fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)), + onConfirm: confirmEditCustomRules, + ), + ); + } + void confirmAddList({required String name, required String url, required String type}) async { ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingList); @@ -147,10 +200,25 @@ class AddFiltersButton extends StatelessWidget { } } - return widget( - type == 'blacklist' || type == 'whitelist' - ? () => openAddWhitelistBlacklist() - : () => openAddCustomRule(), - ); + switch (type) { + case 'blacklist': + case 'whitelist': + return widget( + () => openAddWhitelistBlacklist(), + ); + + case 'add_custom_rule': + return widget( + () => openAddCustomRule(), + ); + + case 'edit_custom_rule': + return widget( + () => openEditCustomRule(), + ); + + default: + return const SizedBox(); + } } } \ 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 8706f1a..25a1d4c 100644 --- a/lib/screens/filters/custom_rules_list.dart +++ b/lib/screens/filters/custom_rules_list.dart @@ -20,12 +20,12 @@ class CustomRulesList extends StatefulWidget { final void Function(String) onRemoveCustomRule; const CustomRulesList({ - Key? key, + super.key, required this.loadStatus, required this.scrollController, required this.data, required this.onRemoveCustomRule - }) : super(key: key); + }); @override State createState() => _CustomRulesListState(); @@ -210,13 +210,26 @@ class _CustomRulesListState extends State { ); } }, - fab: AddFiltersButton( - type: 'custom_rule', - widget: (fn) => FloatingActionButton( - onPressed: fn, - child: const Icon(Icons.add), - ), + fab: Column( + children: [ + AddFiltersButton( + type: 'edit_custom_rule', + widget: (fn) => FloatingActionButton.small( + onPressed: fn, + child: const Icon(Icons.edit_rounded), + ), + ), + const SizedBox(height: 16), + AddFiltersButton( + type: 'add_custom_rule', + widget: (fn) => FloatingActionButton( + onPressed: fn, + child: const Icon(Icons.add), + ), + ), + ], ), + heightFabHidden: -120, fabVisible: isVisible, ); } diff --git a/lib/screens/filters/modals/add_custom_rule.dart b/lib/screens/filters/modals/add_custom_rule.dart deleted file mode 100644 index abe68bc..0000000 --- a/lib/screens/filters/modals/add_custom_rule.dart +++ /dev/null @@ -1,416 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:segmented_button_slide/segmented_button_slide.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/functions/open_url.dart'; -import 'package:adguard_home_manager/constants/urls.dart'; - -class AddCustomRule extends StatefulWidget { - final void Function(String) onConfirm; - final bool fullScreen; - - const AddCustomRule({ - super.key, - required this.onConfirm, - required this.fullScreen - }); - - @override - State createState() => _AddCustomRuleState(); -} - -enum BlockingPresets { block, unblock, custom } - -class _AddCustomRuleState extends State { - final TextEditingController domainController = TextEditingController(); - String? domainError; - - BlockingPresets preset = BlockingPresets.block; - - bool addImportant = false; - - bool checkValidValues() { - if ( - domainController.text != '' && - domainError == null - ) { - return true; - } - else { - return 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); - } - checkValidValues(); - } - - String buildRule({String?value}) { - String rule = ""; - - String fieldValue = value ?? domainController.text; - - if (preset == BlockingPresets.block) { - rule = "||${fieldValue.trim()}^"; - } - else if (preset == BlockingPresets.unblock) { - rule = "@@||${fieldValue.trim()}^"; - } - else { - rule = fieldValue.trim(); - } - - if (addImportant == true) { - rule = "$rule\$important"; - } - - return rule; - } - - @override - Widget build(BuildContext context) { - - List content() { - return [ - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5 - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Theme.of(context).colorScheme.primary - ) - ), - child: Text( - buildRule(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500 - ), - ) - ), - ], - ), - Container(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: (value) => setState(() => {}), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, - ), - ), - ), - Container(height: 30), - SegmentedButtonSlide( - entries: [ - SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.block), - SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.unblock), - SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.custom), - ], - selectedEntry: preset.index, - onChange: (v) => setState(() => preset = BlockingPresets.values[v]), - colors: SegmentedButtonSlideColors( - barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), - backgroundSelectedColor: Theme.of(context).colorScheme.primary, - foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, - foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, - hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textOverflow: TextOverflow.ellipsis, - fontSize: 14, - height: 40, - margin: const EdgeInsets.symmetric( - horizontal: 24, - ), - ), - Container(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: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - Switch( - value: addImportant, - onChanged: (value) => setState(() => addImportant = value), - ) - ], - ), - ), - ), - ), - Container(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Row( - children: [ - Icon( - Icons.info, - color: Theme.of(context).colorScheme.onSurface - ), - const SizedBox(width: 20), - Text( - AppLocalizations.of(context)!.examples, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - 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).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example1, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "@@||example.org^", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example2, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "! Here goes a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - Text( - "# Also a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example3, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "/REGEX/", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example4, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - ], - ), - ) - ], - ), - ), - ), - ), - Container(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => openUrl(Urls.customRuleDocs), - 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: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 15), - child: Icon( - Icons.open_in_new, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - ), - ), - ), - Container(height: 20) - ]; - } - - if (widget.fullScreen == true) { - return Dialog.fullscreen( - child: Scaffold( - appBar: AppBar( - leading: CloseButton(onPressed: () => Navigator.pop(context)), - title: Text(AppLocalizations.of(context)!.addCustomRule), - actions: [ - IconButton( - onPressed: checkValidValues() == true - ? () { - Navigator.pop(context); - widget.onConfirm(buildRule()); - } - : null, - icon: const Icon(Icons.check) - ), - const SizedBox(width: 10) - ], - ), - body: SafeArea( - child: ListView( - children: content(), - ), - ) - ), - ); - } - else { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.clear_rounded), - tooltip: AppLocalizations.of(context)!.close, - ), - const SizedBox(width: 8), - Text( - AppLocalizations.of(context)!.addCustomRule, - style: const TextStyle( - fontSize: 22 - ), - ), - ], - ), - IconButton( - onPressed: checkValidValues() == true - ? () { - Navigator.pop(context); - widget.onConfirm(buildRule()); - } - : null, - icon: const Icon(Icons.check) - ) - ], - ), - ), - Flexible( - child: SingleChildScrollView( - child: Wrap( - alignment: WrapAlignment.center, - children: content(), - ), - ), - ) - ], - ), - ), - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart b/lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart new file mode 100644 index 0000000..ad43ebb --- /dev/null +++ b/lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart @@ -0,0 +1,326 @@ +import 'package:flutter/material.dart'; +import 'package:segmented_button_slide/segmented_button_slide.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/modals/add_custom_rule/custom_rule_docs.dart'; + +enum _BlockingPresets { block, unblock, custom } + +class AddCustomRule extends StatefulWidget { + final void Function(String) onConfirm; + final bool fullScreen; + + const AddCustomRule({ + super.key, + required this.onConfirm, + required this.fullScreen + }); + + @override + State createState() => _AddCustomRuleState(); +} + +class _AddCustomRuleState extends State { + final TextEditingController _domainController = TextEditingController(); + String? _domainError; + + _BlockingPresets _preset = _BlockingPresets.block; + + bool _addImportant = false; + + bool _checkValidValues() { + if ( + _domainController.text != '' && + _domainError == null + ) { + return true; + } + else { + return 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); + } + _checkValidValues(); + } + + @override + Widget build(BuildContext context) { + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton(onPressed: () => Navigator.pop(context)), + title: Text(AppLocalizations.of(context)!.addCustomRule), + actions: [ + IconButton( + onPressed: _checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm( + _buildRule( + domainController: _domainController, + important: _addImportant, + preset: _preset + ) + ); + } + : null, + icon: const Icon(Icons.check) + ), + const SizedBox(width: 10) + ], + ), + body: SafeArea( + child: ListView( + children: [ + _CustomRuleEditor( + domainController: _domainController, + domainError: _domainError, + important: _addImportant, + preset: _preset, + setImportant: (v) => setState(() => _addImportant = v), + setPreset: (v) => setState(() => _preset = v), + validateDomain: validateDomain + ) + ] + ), + ) + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.addCustomRule, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + IconButton( + onPressed: _checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm( + _buildRule( + domainController: _domainController, + important: _addImportant, + preset: _preset + ) + ); + } + : null, + icon: const Icon(Icons.check) + ) + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + alignment: WrapAlignment.center, + children: [ + _CustomRuleEditor( + domainController: _domainController, + domainError: _domainError, + important: _addImportant, + preset: _preset, + setImportant: (v) => setState(() => _addImportant = v), + setPreset: (v) => setState(() => _preset = v), + validateDomain: validateDomain + ) + ] + ), + ), + ) + ], + ), + ), + ); + } + } +} + +class _CustomRuleEditor extends StatelessWidget { + final TextEditingController domainController; + final String? domainError; + final bool important; + final void Function(bool) setImportant; + final _BlockingPresets preset; + final void Function(_BlockingPresets) setPreset; + final void Function(String) validateDomain; + + const _CustomRuleEditor({ + required this.domainController, + required this.domainError, + required this.important, + required this.setImportant, + required this.preset, + required this.setPreset, + required this.validateDomain, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).colorScheme.primary + ) + ), + child: Text( + _buildRule( + domainController: domainController, + important: important, + preset: preset, + ), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500 + ), + ) + ), + ], + ), + Container(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + 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, + ), + ), + ), + Container(height: 30), + SegmentedButtonSlide( + entries: [ + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.block), + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.unblock), + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.custom), + ], + selectedEntry: preset.index, + onChange: (v) => setPreset(_BlockingPresets.values[v]), + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, + foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, + hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textOverflow: TextOverflow.ellipsis, + fontSize: 14, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, + ), + ), + Container(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setImportant(!important), + 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: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Switch( + value: important, + onChanged: setImportant, + ) + ], + ), + ), + ), + ), + Container(height: 20), + const CustomRuleDocs(), + Container(height: 20) + ] + ); + } +} + + String _buildRule({ + String? value, + required TextEditingController domainController, + required _BlockingPresets preset, + required bool important +}) { + String rule = ""; + String fieldValue = value ?? domainController.text; + if (preset == _BlockingPresets.block) { + rule = "||${fieldValue.trim()}^"; + } + else if (preset == _BlockingPresets.unblock) { + rule = "@@||${fieldValue.trim()}^"; + } + else { + rule = fieldValue.trim(); + } + if (important == true) { + rule = "$rule\$important"; + } + return rule; + } \ No newline at end of file diff --git a/lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart b/lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart new file mode 100644 index 0000000..3d2c359 --- /dev/null +++ b/lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; + +class CustomRuleDocs extends StatelessWidget { + const CustomRuleDocs({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.info, + color: Theme.of(context).colorScheme.onSurface + ), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.examples, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + 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).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example1, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "@@||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example2, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "! Here goes a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + Text( + "# Also a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example3, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "/REGEX/", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example4, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + ], + ), + ) + ], + ), + ), + ), + ), + Container(height: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => openUrl(Urls.customRuleDocs), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.moreInformation, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 15), + child: Icon( + Icons.open_in_new, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/modals/add_custom_rule/edit_custom_rules.dart b/lib/screens/filters/modals/add_custom_rule/edit_custom_rules.dart new file mode 100644 index 0000000..e1fc1d1 --- /dev/null +++ b/lib/screens/filters/modals/add_custom_rule/edit_custom_rules.dart @@ -0,0 +1,167 @@ +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/modals/add_custom_rule/custom_rule_docs.dart'; + +import 'package:adguard_home_manager/providers/filtering_provider.dart'; + +class EditCustomRules extends StatefulWidget { + final bool fullScreen; + final void Function(List) onConfirm; + + const EditCustomRules({ + super.key, + required this.fullScreen, + required this.onConfirm, + }); + + @override + State createState() => _EditCustomRulesState(); +} + +class _EditCustomRulesState extends State { + final _fieldController = TextEditingController(); + + @override + void initState() { + final filteringProvider = Provider.of(context, listen: false); + print(filteringProvider.filtering!.userRules); + if (filteringProvider.filtering != null) { + _fieldController.text = filteringProvider.filtering!.userRules.join("\n"); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton(onPressed: () => Navigator.pop(context)), + title: Text(AppLocalizations.of(context)!.editCustomRules), + actions: [ + IconButton( + onPressed: () { + Navigator.pop(context); + widget.onConfirm(_fieldController.text.split("\n")); + }, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ), + body: SafeArea( + child: ListView( + children: [ + _CustomRulesRawEditor(fieldController: _fieldController) + ] + ), + ) + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.addCustomRule, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + IconButton( + onPressed: () { + Navigator.pop(context); + widget.onConfirm(_fieldController.text.split("\n")); + }, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ) + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + alignment: WrapAlignment.center, + children: [ + _CustomRulesRawEditor(fieldController: _fieldController) + ] + ), + ), + ) + ], + ), + ), + ); + } + } +} + +class _CustomRulesRawEditor extends StatelessWidget { + final TextEditingController fieldController; + + const _CustomRulesRawEditor({ + required this.fieldController + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 24), + SizedBox( + height: 300, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: TextField( + controller: fieldController, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.rules, + floatingLabelBehavior: FloatingLabelBehavior.always + ), + autocorrect: false, + expands: true, + minLines: null, + maxLines: null, + textAlignVertical: TextAlignVertical.top, + ), + ), + ), + const SizedBox(height: 24), + const CustomRuleDocs(), + const SizedBox(height: 16), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tab_content_list.dart b/lib/widgets/tab_content_list.dart index 92b7913..0e09a4f 100644 --- a/lib/widgets/tab_content_list.dart +++ b/lib/widgets/tab_content_list.dart @@ -17,6 +17,7 @@ class CustomTabContentList extends StatelessWidget { final bool? fabVisible; final bool? noSliver; final EdgeInsets? listPadding; + final double? heightFabHidden; const CustomTabContentList({ super.key, @@ -31,7 +32,8 @@ class CustomTabContentList extends StatelessWidget { this.fab, this.fabVisible, this.noSliver, - this.listPadding + this.listPadding, + this.heightFabHidden, }); @override @@ -150,7 +152,7 @@ class CustomTabContentList extends StatelessWidget { bottom: fabVisible != null && fabVisible == true ? appConfigProvider.showingSnackbar ? 90 : 20 - : -90, + : (heightFabHidden ?? -90), right: 20, child: SafeArea(child: fab!) ),