From 38172a2c1c445a48a97f7eefc5bbaf39ded0fc71 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Wed, 12 Apr 2023 23:26:50 +0200 Subject: [PATCH] Added timer disable general --- lib/functions/time_server_disabled.dart | 11 + lib/l10n/app_en.arb | 9 +- lib/l10n/app_es.arb | 9 +- lib/models/server_status.dart | 9 + lib/providers/servers_provider.dart | 27 ++- lib/screens/filters/filters.dart | 5 +- lib/screens/home/management_modal.dart | 283 ++++++++++++++++++++---- lib/services/http_requests.dart | 16 +- 8 files changed, 317 insertions(+), 52 deletions(-) create mode 100644 lib/functions/time_server_disabled.dart diff --git a/lib/functions/time_server_disabled.dart b/lib/functions/time_server_disabled.dart new file mode 100644 index 0000000..07b6f2b --- /dev/null +++ b/lib/functions/time_server_disabled.dart @@ -0,0 +1,11 @@ +DateTime generateTimeDeadline(int time) { + DateTime date = DateTime.now(); + date = date.add(Duration(milliseconds: time)); + return date; +} + +String generateRemainingTimeString(Duration difference) { + final int seconds = difference.inSeconds+1; + final DateTime time = DateTime(0, 0, 0, 0, 0, seconds > 0 ? seconds : 0); + return "${time.hour > 0 ? "${time.hour}:" : ''}${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}"; +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f99ceb6..d42e8e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -355,7 +355,7 @@ "gateway": "Gateway", "gatewayNotValid": "Gateway not valid", "leaseTime": "Lease time", - "seconds": "seconds", + "seconds": "{time} seconds", "leaseTimeNotValid": "Lease time not valid", "restoreConfiguration": "Reset configuration", "restoreConfigurationMessage": "Are you sure you want to continue? This will reset all the configuration. This action cannot be undone.", @@ -598,5 +598,10 @@ "checkUpdates": "Check updates", "requestingUpdate": "Requesting update...", "autoupdateUnavailable": "Autoupdate unavailable", - "autoupdateUnavailableDescription": "The autoupdate service is not available for this server. It could be because the server is running on a Docker container. You have to update your server manually." + "autoupdateUnavailableDescription": "The autoupdate service is not available for this server. It could be because the server is running on a Docker container. You have to update your server manually.", + "minute": "{time} minute", + "minutes": "{time} minutes", + "hour": "{time} hour", + "hours": "{time} hours", + "remainingTime": "Remaining time" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9a48639..6835437 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -355,7 +355,7 @@ "gateway": "Puerta de enlace", "gatewayNotValid": "Puerta de enlace no válida", "leaseTime": "Tiempo de asignación", - "seconds": "segundos", + "seconds": "{time} segundos", "leaseTimeNotValid": "Tiempo de asignación no válido", "restoreConfiguration": "Restaurar configuración", "restoreConfigurationMessage": "¿Estás seguro de que deseas continuar? Se reseteará toda la configuración. Esta acción no se puede deshacer.", @@ -598,5 +598,10 @@ "checkUpdates": "Comprobar actualizaciones", "requestingUpdate": "Solicitando actualización...", "autoupdateUnavailable": "Autoactualización no disponible", - "autoupdateUnavailableDescription": "El servicio de actualización automática del servidor no está disponible. Puede ser porque el servidor se esté ejecutando en un contenedor Docker. Tienes que actualizar tu servidor manualmente." + "autoupdateUnavailableDescription": "El servicio de actualización automática del servidor no está disponible. Puede ser porque el servidor se esté ejecutando en un contenedor Docker. Tienes que actualizar tu servidor manualmente.", + "minute": "{time} minuto", + "minutes": "{time} minutos", + "hour": "{time} hora", + "hours": "{time} horas", + "remainingTime": "Tiempo restante" } \ No newline at end of file diff --git a/lib/models/server_status.dart b/lib/models/server_status.dart index fec00b3..8d76613 100644 --- a/lib/models/server_status.dart +++ b/lib/models/server_status.dart @@ -1,3 +1,4 @@ +import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/dns_statistics.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; @@ -15,6 +16,8 @@ class ServerStatusData { final DnsStatistics stats; final List clients; final FilteringStatus filteringStatus; + int timeGeneralDisabled; + DateTime? disabledUntil; bool generalEnabled; bool filteringEnabled; bool safeSearchEnabled; @@ -25,6 +28,8 @@ class ServerStatusData { required this.stats, required this.clients, required this.filteringStatus, + required this.timeGeneralDisabled, + this.disabledUntil, required this.generalEnabled, required this.filteringEnabled, required this.safeSearchEnabled, @@ -36,6 +41,10 @@ class ServerStatusData { stats: DnsStatistics.fromJson(json['stats']), clients: json["clients"] != null ? List.from(json["clients"].map((x) => Client.fromJson(x))) : [], generalEnabled: json['generalEnabled']['protection_enabled'], + timeGeneralDisabled: json['generalEnabled']['protection_disabled_duration'] ?? 0, + disabledUntil: json['generalEnabled']['protection_disabled_duration'] > 0 + ? generateTimeDeadline(json['generalEnabled']['protection_disabled_duration']) + : null , filteringStatus: FilteringStatus.fromJson(json['filtering']), filteringEnabled: json['filtering']['enabled'], safeSearchEnabled: json['safeSearchEnabled']['enabled'], diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index b02a154..471bcaf 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -13,6 +13,7 @@ import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:adguard_home_manager/functions/conversions.dart'; import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/constants/enums.dart'; @@ -328,18 +329,35 @@ class ServersProvider with ChangeNotifier { } } - Future updateBlocking(Server server, String block, bool newStatus) async { + Future updateBlocking({ + required Server server, + required String block, + required bool newStatus, + int? time + }) async { switch (block) { case 'general': _protectionsManagementProcess.add('general'); notifyListeners(); - final result = await updateGeneralProtection(server, newStatus); + final result = await updateGeneralProtection( + server: server, + enable: newStatus, + time: time + ); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'general').toList(); if (result['result'] == 'success') { _serverStatus.data!.generalEnabled = newStatus; + if (time != null) { + _serverStatus.data!.timeGeneralDisabled = time; + _serverStatus.data!.disabledUntil = generateTimeDeadline(time); + } + else { + _serverStatus.data!.timeGeneralDisabled = 0; + _serverStatus.data!.disabledUntil = null; + } notifyListeners(); return null; } @@ -352,7 +370,10 @@ class ServersProvider with ChangeNotifier { _protectionsManagementProcess.add('filtering'); notifyListeners(); - final result = await updateFiltering(server, newStatus); + final result = await updateFiltering( + server: server, + enable: newStatus, + ); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'filtering').toList(); diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index e69728c..f985f48 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -156,7 +156,10 @@ class _FiltersWidgetState extends State with TickerProviderStateM : AppLocalizations.of(context)!.enableFiltering ); - final result = await updateFiltering(serversProvider.selectedServer!, !serversProvider.serverStatus.data!.filteringEnabled); + final result = await updateFiltering( + server: serversProvider.selectedServer!, + enable: !serversProvider.serverStatus.data!.filteringEnabled + ); processModal.close(); diff --git a/lib/screens/home/management_modal.dart b/lib/screens/home/management_modal.dart index 15664c5..ad40b89 100644 --- a/lib/screens/home/management_modal.dart +++ b/lib/screens/home/management_modal.dart @@ -1,27 +1,133 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:async'; import 'dart:io'; +import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:expandable/expandable.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -class ManagementModal extends StatelessWidget { +class ManagementModal extends StatefulWidget { const ManagementModal({Key? key}) : super(key: key); + @override + State createState() => _ManagementModalState(); +} + +class _ManagementModalState extends State with SingleTickerProviderStateMixin { + double height = 540; + bool showTimes = false; + late AnimationController animationController; + late Animation animation; + final ExpandableController expandableController = ExpandableController(); + + DateTime? currentDeadline; + Timer? countdown; + int start = 0; + + @override + void initState() { + expandableController.addListener(() async { + await Future.delayed(const Duration(milliseconds: 200)); + if (expandableController.value == false) { + animationController.animateTo(0); + } + else { + animationController.animateBack(1); + } + }); + + animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ) + ..addListener(() => setState(() => {})); + animation = Tween( + begin: 0.0, + end: 0.5, + ).animate(CurvedAnimation( + parent: animationController, + curve: Curves.easeInOut + )); + + super.initState(); + } + + @override + void dispose() { + if (countdown != null) countdown!.cancel(); + animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - void updateBlocking(bool value, String filter) async { + void startTimer(DateTime deadline) { + setState(() { + currentDeadline = deadline; + start = deadline.difference(DateTime.now()).inSeconds+1; + }); + + const oneSec = Duration(seconds: 1); + countdown = Timer.periodic( + oneSec, + (Timer timer) async { + if (start == 0) { + setState(() { + timer.cancel(); + }); + final result = await getServerStatus(serversProvider.selectedServer!); + if (result['result'] == 'success') { + serversProvider.setServerStatusData(result['data']); + } + } else { + setState(() { + start = start - 1; + }); + } + }, + ); + } + + if ( + serversProvider.serverStatus.data != null && + serversProvider.serverStatus.data!.disabledUntil != null && + serversProvider.serverStatus.data!.disabledUntil != currentDeadline + ) { + startTimer(serversProvider.serverStatus.data!.disabledUntil!); + } + + if ( + serversProvider.serverStatus.data != null && + serversProvider.serverStatus.data!.generalEnabled == true + ) { + setState(() { + start = 0; + currentDeadline = null; + if (countdown != null) countdown!.cancel(); + countdown = null; + }); + } + + void updateBlocking({ + required bool value, + required String filter, + int? time + }) async { final result = await serversProvider.updateBlocking( - serversProvider.selectedServer!, - filter, - value + server: serversProvider.selectedServer!, + block: filter, + newStatus: value, + time: time ); if (result != null) { if (result != false) { @@ -36,42 +142,139 @@ class ManagementModal extends StatelessWidget { } } + void disableWithCountdown(int time) { + updateBlocking(value: false, filter: 'general', time: time); + expandableController.toggle(); + } + Widget mainSwitch() { + Widget topRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + RotationTransition( + turns: animation, + child: Icon( + Icons.keyboard_arrow_down_rounded, + size: 26, + color: serversProvider.serverStatus.data!.generalEnabled == true + ? Theme.of(context).colorScheme.onSurfaceVariant + : Colors.grey, + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.allProtections, + style: const TextStyle( + fontSize: 18, + ), + ), + if (serversProvider.serverStatus.data!.timeGeneralDisabled > 0) ...[ + const SizedBox(height: 2), + if (currentDeadline != null) Text( + "${AppLocalizations.of(context)!.remainingTime}: ${generateRemainingTimeString(currentDeadline!.difference(DateTime.now()))}" + ) + ] + ], + ), + ], + ), + Switch( + value: serversProvider.serverStatus.data!.generalEnabled, + onChanged: serversProvider.protectionsManagementProcess.contains('general') == false + ? (value) => updateBlocking(value: value, filter: 'general') + : null, + activeColor: Theme.of(context).colorScheme.primary, + ) + ] + ); + } + + Widget bottomRow() { + return Container( + height: 40, + margin: const EdgeInsets.only(top: 8), + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + ActionChip( + label: Text(AppLocalizations.of(context)!.seconds(30)), + onPressed: serversProvider.protectionsManagementProcess.contains('general') == false + ? () => disableWithCountdown(29000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minute(1)), + onPressed: serversProvider.protectionsManagementProcess.contains('general') == false + ? () => disableWithCountdown(59000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minutes(10)), + onPressed: serversProvider.protectionsManagementProcess.contains('general') == false + ? () => disableWithCountdown(599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hour(1)), + onPressed: serversProvider.protectionsManagementProcess.contains('general') == false + ? () => disableWithCountdown(3599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hours(24)), + onPressed: serversProvider.protectionsManagementProcess.contains('general') == false + ? () => disableWithCountdown(86399000) + : null, + ), + ], + ), + ); + } + return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: serversProvider.protectionsManagementProcess.contains('general') == false - ? () => updateBlocking(!serversProvider.serverStatus.data!.generalEnabled, 'general') - : null, + child: ExpandableNotifier( + controller: expandableController, + child: Material( + color: Colors.transparent, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.allProtections, - style: const TextStyle( - fontSize: 18, - ), - ), - Switch( - value: serversProvider.serverStatus.data!.generalEnabled, - onChanged: serversProvider.protectionsManagementProcess.contains('general') == false - ? (value) => updateBlocking(value, 'general') - : null, - activeColor: Theme.of(context).colorScheme.primary, + child: InkWell( + onTap: serversProvider.serverStatus.data!.generalEnabled == true + ? () => expandableController.toggle() + : null, + borderRadius: BorderRadius.circular(28), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: Theme.of(context).primaryColor.withOpacity(0.1) + ), + child: Expandable( + collapsed: topRow(), + expanded: Column( + children: [ + topRow(), + bottomRow(), + const SizedBox(height: 8) + ], ) - ], + ), ), ), - ), + ) ), ); } @@ -125,7 +328,7 @@ class ManagementModal extends StatelessWidget { return Container( width: double.maxFinite, - height: Platform.isIOS ? 556 : 540, + height: Platform.isIOS ? height+16 : height, decoration: BoxDecoration( color: Theme.of(context).dialogBackgroundColor, borderRadius: const BorderRadius.only( @@ -137,7 +340,7 @@ class ManagementModal extends StatelessWidget { children: [ Expanded( child: ListView( - physics: (Platform.isIOS ? 556 : 540) < MediaQuery.of(context).size.height + physics: (Platform.isIOS ? height+16 : height) < MediaQuery.of(context).size.height ? const NeverScrollableScrollPhysics() : null, children: [ @@ -166,28 +369,28 @@ class ManagementModal extends StatelessWidget { AppLocalizations.of(context)!.ruleFiltering, Icons.filter_list_rounded, serversProvider.serverStatus.data!.filteringEnabled, - (value) => updateBlocking(value, 'filtering'), + (value) => updateBlocking(value: value, filter: 'filtering'), serversProvider.protectionsManagementProcess.contains('filtering') ), smallSwitch( AppLocalizations.of(context)!.safeBrowsing, Icons.vpn_lock_rounded, serversProvider.serverStatus.data!.safeBrowsingEnabled, - (value) => updateBlocking(value, 'safeBrowsing'), + (value) => updateBlocking(value: value, filter: 'safeBrowsing'), serversProvider.protectionsManagementProcess.contains('safeBrowsing') ), smallSwitch( AppLocalizations.of(context)!.parentalFiltering, Icons.block, serversProvider.serverStatus.data!.parentalControlEnabled, - (value) => updateBlocking(value, 'parentalControl'), + (value) => updateBlocking(value: value, filter: 'parentalControl'), serversProvider.protectionsManagementProcess.contains('parentalControl') ), smallSwitch( AppLocalizations.of(context)!.safeSearch, Icons.search_rounded, serversProvider.serverStatus.data!.safeSearchEnabled, - (value) => updateBlocking(value, 'safeSearch'), + (value) => updateBlocking(value: value, filter: 'safeSearch'), serversProvider.protectionsManagementProcess.contains('safeSearch') ), ], diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 27780f6..b1b3d25 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -318,7 +318,10 @@ Future getServerStatus(Server server) async { } } -Future updateFiltering(Server server, bool enable) async { +Future updateFiltering({ + required Server server, + required bool enable, +}) async { final result = await apiRequest( urlPath: '/filtering/config', method: 'post', @@ -462,13 +465,18 @@ Future updateParentalControl(Server server, bool enable) async { } } -Future updateGeneralProtection(Server server, bool enable) async { +Future updateGeneralProtection({ + required Server server, + required bool enable, + int? time +}) async { final result = await apiRequest( - urlPath: '/dns_config', + urlPath: '/protection', method: 'post', server: server, body: { - 'protection_enabled': enable + 'enabled': enable, + 'duration': time }, type: 'general_protection' );