mirror of
https://github.com/JGeek00/adguard-home-manager.git
synced 2025-04-21 22:39:11 +00:00
Added edit custom rules
This commit is contained in:
parent
2fca44f195
commit
c150d2ba23
10 changed files with 789 additions and 436 deletions
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
326
lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart
Normal file
326
lib/screens/filters/modals/add_custom_rule/add_custom_rule.dart
Normal 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;
|
||||
}
|
166
lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart
Normal file
166
lib/screens/filters/modals/add_custom_rule/custom_rule_docs.dart
Normal 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
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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!)
|
||||
),
|
||||
|
|
Loading…
Add table
Reference in a new issue