diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bbb19bc..6ae74fe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -468,5 +468,6 @@ "blockingIpv4": "Blocking IPv4", "blockingIpv4Description": "IP address to be returned for a blocked A request", "blockingIpv6": "Blocking IPv6", - "blockingIpv6Description": "IP address to be returned for a blocked AAAA request" + "blockingIpv6Description": "IP address to be returned for a blocked AAAA request", + "invalidIp": "Invalid IP address" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5410d90..0f1d772 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -468,5 +468,6 @@ "blockingIpv4": "Bloqueo de IPv4", "blockingIpv4Description": "Dirección IP devolverá una petición A bloqueada", "blockingIpv6": "Bloqueo de IPv6", - "blockingIpv6Description": "Dirección IP devolverá una petición AAAA bloqueada" + "blockingIpv6Description": "Dirección IP devolverá una petición AAAA bloqueada", + "invalidIp": "Dirección IP no válida" } \ No newline at end of file diff --git a/lib/screens/settings/dns/bootstrap_dns.dart b/lib/screens/settings/dns/bootstrap_dns.dart index f8bbc4b..99efa1b 100644 --- a/lib/screens/settings/dns/bootstrap_dns.dart +++ b/lib/screens/settings/dns/bootstrap_dns.dart @@ -16,15 +16,45 @@ class BootstrapDnsScreen extends StatefulWidget { } class _BootstrapDnsScreenState extends State { - List bootstrapControllers = []; + List> bootstrapControllers = []; + + bool validValues = false; + + void validateIp(Map field, String value) { + RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)'); + if (ipAddress.hasMatch(value) == true) { + setState(() => field['error'] = null); + } + else { + setState(() => field['error'] = AppLocalizations.of(context)!.invalidIp); + } + checkValidValues(); + } + + void checkValidValues() { + if ( + bootstrapControllers.isNotEmpty && + bootstrapControllers.every((element) => element['controller'].text != '') && + bootstrapControllers.every((element) => element['error'] == null) + ) { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } @override void initState() { for (var item in widget.serversProvider.dnsInfo.data!.bootstrapDns) { final controller = TextEditingController(); controller.text = item; - bootstrapControllers.add(controller); + bootstrapControllers.add({ + 'controller': controller, + 'error': null + }); } + validValues = true; super.initState(); } @@ -33,6 +63,16 @@ class _BootstrapDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.bootstrapDns), + actions: [ + IconButton( + onPressed: validValues == true + ? () => {} + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], ), body: ListView( padding: const EdgeInsets.only(top: 10), @@ -83,8 +123,8 @@ class _BootstrapDnsScreenState extends State { SizedBox( width: MediaQuery.of(context).size.width-90, child: TextFormField( - controller: c, - // onChanged: (_) => checkValidValues(), + controller: c['controller'], + onChanged: (value) => validateIp(c, value), decoration: InputDecoration( prefixIcon: const Icon(Icons.dns_rounded), border: const OutlineInputBorder( @@ -92,12 +132,16 @@ class _BootstrapDnsScreenState extends State { Radius.circular(10) ) ), + errorText: c['error'], labelText: AppLocalizations.of(context)!.dnsServer, ) ), ), IconButton( - onPressed: () => setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()), + onPressed: () { + setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()); + checkValidValues(); + }, icon: const Icon(Icons.remove_circle_outline) ) ], @@ -108,7 +152,13 @@ class _BootstrapDnsScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( - onPressed: () => setState(() => bootstrapControllers.add(TextEditingController())), + onPressed: () { + setState(() => bootstrapControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkValidValues(); + }, icon: const Icon(Icons.add), label: Text(AppLocalizations.of(context)!.addItem) ), diff --git a/lib/screens/settings/dns/cache_config.dart b/lib/screens/settings/dns/cache_config.dart index 13c5b60..8c007b2 100644 --- a/lib/screens/settings/dns/cache_config.dart +++ b/lib/screens/settings/dns/cache_config.dart @@ -29,12 +29,31 @@ class _CacheConfigDnsScreenState extends State { bool optimisticCache = false; + bool validData = false; + + void checkValidData() { + if ( + cacheSizeController.text != '' && + cacheSizeError == null && + overrideMinTtlController.text != '' && + overrideMinTtlError == null && + overrideMaxTtlController.text != '' && + overrideMaxTtlError == null + ) { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + } + @override void initState() { cacheSizeController.text = widget.serversProvider.dnsInfo.data!.cacheSize.toString(); overrideMinTtlController.text = widget.serversProvider.dnsInfo.data!.cacheTtlMin.toString(); overrideMaxTtlController.text = widget.serversProvider.dnsInfo.data!.cacheTtlMax.toString(); optimisticCache = widget.serversProvider.dnsInfo.data!.cacheOptimistic; + validData = true; super.initState(); } @@ -73,6 +92,16 @@ class _CacheConfigDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsCacheConfig), + actions: [ + IconButton( + onPressed: validData == true + ? () => {} + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], ), body: ListView( padding: const EdgeInsets.only(top: 10), @@ -89,6 +118,7 @@ class _CacheConfigDnsScreenState extends State { else { setState(() => cacheSizeError = AppLocalizations.of(context)!.valueNotNumber); } + checkValidData(); } ), const SizedBox(height: 30), @@ -104,6 +134,7 @@ class _CacheConfigDnsScreenState extends State { else { setState(() => overrideMinTtlError = AppLocalizations.of(context)!.valueNotNumber); } + checkValidData(); } ), const SizedBox(height: 30), @@ -119,6 +150,7 @@ class _CacheConfigDnsScreenState extends State { else { setState(() => overrideMaxTtlError = AppLocalizations.of(context)!.valueNotNumber); } + checkValidData(); } ), const SizedBox(height: 10), diff --git a/lib/screens/settings/dns/dns_server_settings.dart b/lib/screens/settings/dns/dns_server_settings.dart index 771a572..dbedc11 100644 --- a/lib/screens/settings/dns/dns_server_settings.dart +++ b/lib/screens/settings/dns/dns_server_settings.dart @@ -33,6 +33,52 @@ class _DnsServerSettingsScreenState extends State { final TextEditingController ipv6controller = TextEditingController(); String? ipv6error; + bool isDataValid = false; + + void validateIpv4(String value) { + RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$'); + if (ipAddress.hasMatch(value) == true) { + setState(() => ipv4error = null); + } + else { + setState(() => ipv4error = AppLocalizations.of(context)!.invalidIp); + } + validateData(); + } + + void validateIpv6(String value) { + RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)'); + if (ipAddress.hasMatch(value) == true) { + setState(() => ipv6error = null); + } + else { + setState(() => ipv6error = AppLocalizations.of(context)!.invalidIp); + } + validateData(); + } + + void validateData() { + if ( + limitRequestsController.text != '' && + limitRequestsError == null && + ( + blockingMode != 'custom_ip' || + ( + blockingMode == 'custom_ip' && + ipv4controller.text != '' && + ipv4error == null && + ipv6controller.text != '' && + ipv6error == null + ) + ) == true + ) { + setState(() => isDataValid = true); + } + else { + setState(() => isDataValid = false); + } + } + @override void initState() { limitRequestsController.text = widget.serversProvider.dnsInfo.data!.ratelimit.toString(); @@ -42,6 +88,7 @@ class _DnsServerSettingsScreenState extends State { blockingMode = widget.serversProvider.dnsInfo.data!.blockingMode; ipv4controller.text = widget.serversProvider.dnsInfo.data!.blockingIpv4; ipv6controller.text = widget.serversProvider.dnsInfo.data!.blockingIpv6; + isDataValid = true; super.initState(); } @@ -56,11 +103,22 @@ class _DnsServerSettingsScreenState extends State { ipv6error = null; } setState(() => blockingMode = mode); + validateData(); } return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsServerSettings), + actions: [ + IconButton( + onPressed: isDataValid == true + ? () => {} + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], ), body: ListView( padding: const EdgeInsets.only(top: 10), @@ -76,6 +134,7 @@ class _DnsServerSettingsScreenState extends State { else { setState(() => limitRequestsError = AppLocalizations.of(context)!.valueNotNumber); } + validateData(); }, decoration: InputDecoration( prefixIcon: const Icon(Icons.looks_one_rounded), @@ -156,7 +215,7 @@ class _DnsServerSettingsScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( controller: ipv4controller, - // onChanged: onChanged, + onChanged: validateIpv4, decoration: InputDecoration( prefixIcon: const Icon(Icons.link_rounded), border: const OutlineInputBorder( @@ -177,7 +236,7 @@ class _DnsServerSettingsScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( controller: ipv6controller, - // onChanged: onChanged, + onChanged: validateIpv6, decoration: InputDecoration( prefixIcon: const Icon(Icons.link_rounded), border: const OutlineInputBorder( diff --git a/lib/screens/settings/dns/private_reverse_servers.dart b/lib/screens/settings/dns/private_reverse_servers.dart index 3c37f35..d772592 100644 --- a/lib/screens/settings/dns/private_reverse_servers.dart +++ b/lib/screens/settings/dns/private_reverse_servers.dart @@ -20,12 +20,46 @@ class PrivateReverseDnsServersScreen extends StatefulWidget { class _PrivateReverseDnsServersScreenState extends State { List defaultReverseResolvers = []; bool editReverseResolvers = false; - List reverseResolversControllers = [ - TextEditingController() + List> reverseResolversControllers = [ + { + 'controller': TextEditingController(), + 'error': null + } ]; bool usePrivateReverseDnsResolvers = false; bool enableReverseResolve = false; + bool validValues = false; + + void validateAddress(Map item ,String value) { + RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)'); + RegExp domain = RegExp(r'^((http|https|tls|udp|tcp|quic|sdns):\/\/)?([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); + if (ipAddress.hasMatch(value) == true || domain.hasMatch(value) == true) { + setState(() => item['error'] = null); + } + else { + setState(() => item['error'] = AppLocalizations.of(context)!.invalidIpDomain); + } + + checkDataValid(); + } + + void checkDataValid() { + if ( + ( + editReverseResolvers == true && + reverseResolversControllers.isNotEmpty && + reverseResolversControllers.every((element) => element['controller'].text != '') && + reverseResolversControllers.every((element) => element['error'] == null) + ) == true + ) { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } + @override void initState() { for (var item in widget.serversProvider.dnsInfo.data!.defaultLocalPtrUpstreams) { @@ -34,13 +68,17 @@ class _PrivateReverseDnsServersScreenState extends State {} + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], ), body: ListView( padding: const EdgeInsets.only(top: 10), @@ -90,7 +138,10 @@ class _PrivateReverseDnsServersScreenState extends State setState(() => editReverseResolvers = true), + onPressed: () { + setState(() => editReverseResolvers = true); + checkDataValid(); + }, icon: const Icon(Icons.edit), label: Text(AppLocalizations.of(context)!.edit) ), @@ -110,8 +161,8 @@ class _PrivateReverseDnsServersScreenState extends State checkValidValues(), + controller: c['controller'], + onChanged: (value) => validateAddress(c, value), decoration: InputDecoration( prefixIcon: const Icon(Icons.dns_rounded), border: const OutlineInputBorder( @@ -119,12 +170,16 @@ class _PrivateReverseDnsServersScreenState extends State setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()), + onPressed: () { + setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()); + checkDataValid(); + }, icon: const Icon(Icons.remove_circle_outline) ) ], @@ -152,7 +207,13 @@ class _PrivateReverseDnsServersScreenState extends State setState(() => reverseResolversControllers.add(TextEditingController())), + onPressed: () { + setState(() => reverseResolversControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkDataValid(); + }, icon: const Icon(Icons.add), label: Text(AppLocalizations.of(context)!.addItem) ), diff --git a/lib/screens/settings/dns/upstream_dns.dart b/lib/screens/settings/dns/upstream_dns.dart index 6172c7e..8b416f3 100644 --- a/lib/screens/settings/dns/upstream_dns.dart +++ b/lib/screens/settings/dns/upstream_dns.dart @@ -21,7 +21,21 @@ class UpstreamDnsScreen extends StatefulWidget { class _UpstreamDnsScreenState extends State { List upstreamControllers = []; - String upstreamMode = "load_balancing"; + String upstreamMode = ""; + + bool validValues = false; + + checkValidValues() { + if ( + upstreamControllers.isNotEmpty && + upstreamControllers.every((element) => element.text != '') + ) { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } @override void initState() { @@ -31,6 +45,7 @@ class _UpstreamDnsScreenState extends State { upstreamControllers.add(controller); } upstreamMode = widget.serversProvider.dnsInfo.data!.upstreamMode; + validValues = true; super.initState(); } @@ -39,6 +54,16 @@ class _UpstreamDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.upstreamDns), + actions: [ + IconButton( + onPressed: validValues == true + ? () => {} + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], ), body: ListView( padding: const EdgeInsets.only(top: 10), @@ -71,7 +96,7 @@ class _UpstreamDnsScreenState extends State { width: MediaQuery.of(context).size.width-90, child: TextFormField( controller: c, - // onChanged: (_) => checkValidValues(), + onChanged: (_) => checkValidValues(), decoration: InputDecoration( prefixIcon: const Icon(Icons.dns_rounded), border: const OutlineInputBorder( @@ -84,7 +109,10 @@ class _UpstreamDnsScreenState extends State { ), ), IconButton( - onPressed: () => setState(() => upstreamControllers = upstreamControllers.where((con) => con != c).toList()), + onPressed: () { + setState(() => upstreamControllers = upstreamControllers.where((con) => con != c).toList()); + checkValidValues(); + }, icon: const Icon(Icons.remove_circle_outline) ) ], @@ -95,7 +123,10 @@ class _UpstreamDnsScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( - onPressed: () => setState(() => upstreamControllers.add(TextEditingController())), + onPressed: () { + setState(() => upstreamControllers.add(TextEditingController())); + checkValidValues(); + }, icon: const Icon(Icons.add), label: Text(AppLocalizations.of(context)!.addItem) ),