From d43398e6e8725abf47c856ac654379b8d0ab7221 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 9 Jul 2023 22:16:25 +0200 Subject: [PATCH] Added edit rewrite rules --- lib/l10n/app_en.arb | 8 +- lib/l10n/app_es.arb | 8 +- lib/providers/rewrite_rules_provider.dart | 27 ++ ...rite_modal.dart => dns_rewrite_modal.dart} | 121 +++++--- .../settings/dns_rewrites/dns_rewrites.dart | 265 ++++++++++++------ .../settings/dns_rewrites/rule_modal.dart | 0 .../dns_rewrites/server_version_needed.dart | 46 +++ lib/services/http_requests.dart | 34 ++- 8 files changed, 391 insertions(+), 118 deletions(-) rename lib/screens/settings/dns_rewrites/{add_dns_rewrite_modal.dart => dns_rewrite_modal.dart} (56%) create mode 100644 lib/screens/settings/dns_rewrites/rule_modal.dart create mode 100644 lib/screens/settings/dns_rewrites/server_version_needed.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1504716..bd82ea1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -630,5 +630,11 @@ "combinedChartDescription": "Combine all charts into one", "statistics": "Statistics", "errorLoadFilters": "Error when loading filters.", - "clientRemovedSuccessfully": "Client removed successfully." + "clientRemovedSuccessfully": "Client removed successfully.", + "editRewriteRule": "Edit rewrite rule", + "dnsRewriteRuleUpdated": "DNS rewrite rule updated successfully", + "dnsRewriteRuleNotUpdated": "DNS rewrite rule could not be updated", + "updatingRule": "Updating rule...", + "serverUpdateNeeded": "Server update needed", + "updateYourServer": "Update your AdGuard Home server to {version} or greater to use this feature." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 88d248f..76f97e9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -630,5 +630,11 @@ "combinedChartDescription": "Combina todos los gráficos en uno solo", "statistics": "Estadísticas", "errorLoadFilters": "Error al cargar los filtros.", - "clientRemovedSuccessfully": "Cliente eliminado correctamente." + "clientRemovedSuccessfully": "Cliente eliminado correctamente.", + "editRewriteRule": "Editar reescritura DNS", + "dnsRewriteRuleUpdated": "Regla de reescritura DNS actualizada correctamente", + "dnsRewriteRuleNotUpdated": "La regla de reescritura DNS no ha podido ser actualizada", + "updatingRule": "Actualizando regla...", + "serverUpdateNeeded": "Actualización del servidor necesaria", + "updateYourServer": "Actualiza tu servidor AdGuard Home a {version} para utilizar esta funcionalidad." } \ No newline at end of file diff --git a/lib/providers/rewrite_rules_provider.dart b/lib/providers/rewrite_rules_provider.dart index 1db40c5..1e3bda3 100644 --- a/lib/providers/rewrite_rules_provider.dart +++ b/lib/providers/rewrite_rules_provider.dart @@ -54,6 +54,33 @@ class RewriteRulesProvider with ChangeNotifier { } } + Future editDnsRewrite(RewriteRules newRule, RewriteRules oldRule) async { + final result = await _serversProvider!.apiClient!.updateRewriteRule( + body: { + "target": { + "answer": oldRule.answer, + "domain": oldRule.domain + }, + "update": { + "answer": newRule.answer, + "domain": newRule.domain + } + } + ); + + if (result['result'] == 'success') { + List data = rewriteRules!; + final index = data.indexOf(oldRule); + data[index] = newRule; + setRewriteRulesData(data); + return true; + } + else { + notifyListeners(); + return false; + } + } + Future deleteDnsRewrite(RewriteRules rule) async { final result = await _serversProvider!.apiClient!.deleteDnsRewriteRule( data: { diff --git a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart similarity index 56% rename from lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart rename to lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart index 24ee717..b43c168 100644 --- a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart @@ -1,25 +1,34 @@ import 'dart:io'; 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/settings/dns_rewrites/server_version_needed.dart'; + +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/models/rewrite_rules.dart'; -class AddDnsRewriteModal extends StatefulWidget { - final void Function(RewriteRules) onConfirm; +class DnsRewriteModal extends StatefulWidget { + final void Function(RewriteRules newRule, RewriteRules? previousRule) onConfirm; final bool dialog; + final RewriteRules? rule; + final void Function(RewriteRules) onDelete; - const AddDnsRewriteModal({ + const DnsRewriteModal({ Key? key, required this.onConfirm, - required this.dialog + required this.dialog, + this.rule, + required this.onDelete }) : super(key: key); @override - State createState() => _AddDnsRewriteModalState(); + State createState() => _AddDnsRewriteModalState(); } -class _AddDnsRewriteModalState extends State { +class _AddDnsRewriteModalState extends State { final TextEditingController domainController = TextEditingController(); String? domainError; final TextEditingController answerController = TextEditingController(); @@ -50,8 +59,20 @@ class _AddDnsRewriteModalState extends State { } } + @override + void initState() { + if (widget.rule != null) { + domainController.text = widget.rule!.domain; + answerController.text = widget.rule!.answer; + validData = true; + } + super.initState(); + } + @override Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + Widget content() { return Column( mainAxisSize: MainAxisSize.min, @@ -68,14 +89,18 @@ class _AddDnsRewriteModalState extends State { Padding( padding: const EdgeInsets.only(top: 24), child: Icon( - Icons.add, + widget.rule != null + ? Icons.edit + : Icons.add, size: 24, color: Theme.of(context).listTileTheme.iconColor ), ), const SizedBox(height: 16), Text( - AppLocalizations.of(context)!.addDnsRewrite, + widget.rule != null + ? AppLocalizations.of(context)!.editRewriteRule + : AppLocalizations.of(context)!.addDnsRewrite, textAlign: TextAlign.center, style: TextStyle( fontSize: 24, @@ -131,34 +156,64 @@ class _AddDnsRewriteModalState extends State { Padding( padding: const EdgeInsets.all(24), child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), + if (widget.rule != null) TextButton( + onPressed: () { + Navigator.pop(context); + widget.onDelete( + RewriteRules( + domain: domainController.text, + answer: answerController.text + ) + ); + }, + child: Text(AppLocalizations.of(context)!.delete), ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm( - RewriteRules( - domain: domainController.text, - answer: answerController.text - ) - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + if (widget.rule == null) const SizedBox(), + Row( + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), ), - ), - ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + if (serverVersionIsAhead( + currentVersion: statusProvider.serverStatus!.serverVersion, + referenceVersion: '0.107.33', + referenceVersionBeta: '0.108.0-b.39' + )) { + Navigator.pop(context); + widget.onConfirm( + RewriteRules( + domain: domainController.text, + answer: answerController.text + ), + widget.rule + ); + } + else { + showDialog( + context: context, + builder: (context) => const ServerVersionNeeded(version: 'v0.107.33') + ); + } + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + ), + ), + ), + ], + ) ], ), ), diff --git a/lib/screens/settings/dns_rewrites/dns_rewrites.dart b/lib/screens/settings/dns_rewrites/dns_rewrites.dart index 17f267e..fda731e 100644 --- a/lib/screens/settings/dns_rewrites/dns_rewrites.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrites.dart @@ -3,11 +3,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart'; import 'package:adguard_home_manager/screens/settings/dns_rewrites/delete_dns_rewrite.dart'; +import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrite_modal.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; @@ -24,10 +25,29 @@ class DnsRewritesScreen extends StatefulWidget { } class _DnsRewritesScreenState extends State { + late bool isVisible; + final ScrollController scrollController = ScrollController(); + @override void initState() { Provider.of(context, listen: false).fetchRules(); super.initState(); + + isVisible = true; + scrollController.addListener(() { + if (scrollController.position.userScrollDirection == ScrollDirection.reverse) { + if (mounted && isVisible == true) { + setState(() => isVisible = false); + } + } + else { + if (scrollController.position.userScrollDirection == ScrollDirection.forward) { + if (mounted && isVisible == false) { + setState(() => isVisible = true); + } + } + } + }); } @override @@ -61,7 +81,7 @@ class _DnsRewritesScreenState extends State { } } - void addDnsRewrite(RewriteRules rule) async { + void addDnsRewrite(RewriteRules rule, _) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.addingRewrite); @@ -85,6 +105,30 @@ class _DnsRewritesScreenState extends State { } } + void updateRewriteRule(RewriteRules newRule, RewriteRules? previousRule) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.updatingRule); + + final result = await rewriteRulesProvider.editDnsRewrite(newRule, previousRule!); + + processModal.close(); + + if (result == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsRewriteRuleUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsRewriteRuleNotUpdated, + color: Colors.red + ); + } + } + Widget generateBody() { switch (rewriteRulesProvider.loadStatus) { case LoadStatus.loading: @@ -121,70 +165,102 @@ class _DnsRewritesScreenState extends State { } }, child: ListView.builder( + controller: scrollController, padding: const EdgeInsets.only(top: 0), itemCount: rewriteRulesProvider.rewriteRules!.length, itemBuilder: (context, index) => Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Padding( - padding: const EdgeInsets.only( - left: 16, top: 16, bottom: 16, right: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.domain}: ", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Text( - rewriteRulesProvider.rewriteRules![index].domain, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ), - ], - ), - const SizedBox(height: 3), - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.answer}: ", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Text( - rewriteRulesProvider.rewriteRules![index].answer, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ), - ], - ), - ], - ), - IconButton( - onPressed: () => { - showDialog( + child: InkWell( + onTap: () => { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: updateRewriteRule, + dialog: true, + rule: rewriteRulesProvider.rewriteRules![index], + onDelete: (rule) => showDialog( context: context, builder: (context) => DeleteDnsRewrite( - onConfirm: () => deleteDnsRewrite(rewriteRulesProvider.rewriteRules![index]) + onConfirm: () => deleteDnsRewrite(rule) ) - ) - }, - icon: const Icon(Icons.delete), - tooltip: AppLocalizations.of(context)!.delete, + ), + ), ) - ], + } + else { + showModalBottomSheet( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: updateRewriteRule, + dialog: false, + rule: rewriteRulesProvider.rewriteRules![index], + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + ) + } + }, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.only( + left: 16, top: 16, bottom: 16, right: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.domain}: ", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Text( + rewriteRulesProvider.rewriteRules![index].domain, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + const SizedBox(height: 3), + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.answer}: ", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Text( + rewriteRulesProvider.rewriteRules![index].answer, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + ], + ), + Icon( + Icons.keyboard_arrow_right_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ), ), ), ) @@ -237,31 +313,56 @@ class _DnsRewritesScreenState extends State { title: Text(AppLocalizations.of(context)!.dnsRewrites), centerTitle: false, ), - body: generateBody(), - floatingActionButton: FloatingActionButton( - onPressed: () => { - if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => AddDnsRewriteModal( - onConfirm: addDnsRewrite, - dialog: true, + body: Stack( + children: [ + generateBody(), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: FloatingActionButton( + onPressed: () => { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: true, + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + ) + } + else { + showModalBottomSheet( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: false, + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ) + } + }, + child: const Icon(Icons.add), ), - ) - } - else { - showModalBottomSheet( - context: context, - builder: (context) => AddDnsRewriteModal( - onConfirm: addDnsRewrite, - dialog: false, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - } - }, - child: const Icon(Icons.add), + ) + ], ), ); } diff --git a/lib/screens/settings/dns_rewrites/rule_modal.dart b/lib/screens/settings/dns_rewrites/rule_modal.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/settings/dns_rewrites/server_version_needed.dart b/lib/screens/settings/dns_rewrites/server_version_needed.dart new file mode 100644 index 0000000..88888c6 --- /dev/null +++ b/lib/screens/settings/dns_rewrites/server_version_needed.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ServerVersionNeeded extends StatelessWidget { + final String version; + + const ServerVersionNeeded({ + Key? key, + required this.version + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.system_update_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.serverUpdateNeeded, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Text( + AppLocalizations.of(context)!.updateYourServer(version), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index cb384b4..85de805 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -2164,7 +2164,6 @@ class ApiClient { } Future updateSafeSearchSettings({ - required Map body }) async { final result = await apiRequest( @@ -2196,6 +2195,39 @@ class ApiClient { return result; } } + + Future updateRewriteRule({ + required Map body + }) async { + final result = await apiRequest( + urlPath: '/rewrite/update', + method: 'put', + server: server, + type: 'update_rewrite', + body: body + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_rewrite', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } } Future checkAppUpdatesGitHub() async {