Added edit custom rules

This commit is contained in:
Juan Gilsanz Polo 2024-02-04 21:23:45 +01:00
parent 2fca44f195
commit c150d2ba23
10 changed files with 789 additions and 436 deletions

View file

@ -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"
}

View file

@ -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"
}

View file

@ -289,6 +289,23 @@ class FilteringProvider with ChangeNotifier {
}
}
Future<bool> setCustomRules(List<String> 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<Map<String, dynamic>> addList({required String name, required String url, required String type}) async {
final result1 = await _serversProvider!.apiClient2!.addFilteringList(
data: {

View file

@ -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<String> 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();
}
}
}

View file

@ -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<CustomRulesList> createState() => _CustomRulesListState();
@ -210,13 +210,26 @@ class _CustomRulesListState extends State<CustomRulesList> {
);
}
},
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,
);
}

View file

@ -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<AddCustomRule> createState() => _AddCustomRuleState();
}
enum BlockingPresets { block, unblock, custom }
class _AddCustomRuleState extends State<AddCustomRule> {
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<Widget> 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(),
),
),
)
],
),
),
);
}
}
}

View file

@ -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<AddCustomRule> createState() => _AddCustomRuleState();
}
class _AddCustomRuleState extends State<AddCustomRule> {
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;
}

View file

@ -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
),
)
],
),
),
),
),
],
);
}
}

View file

@ -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<String>) onConfirm;
const EditCustomRules({
super.key,
required this.fullScreen,
required this.onConfirm,
});
@override
State<EditCustomRules> createState() => _EditCustomRulesState();
}
class _EditCustomRulesState extends State<EditCustomRules> {
final _fieldController = TextEditingController();
@override
void initState() {
final filteringProvider = Provider.of<FilteringProvider>(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),
],
);
}
}

View file

@ -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!)
),