From 78561a68bfae0abd030861f764ac38d0df27d805 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Mon, 10 Oct 2022 14:57:42 +0200 Subject: [PATCH] Added update interval lists modal --- lib/l10n/app_en.arb | 12 +- lib/l10n/app_es.arb | 12 +- lib/models/filtering.dart | 4 +- lib/providers/servers_provider.dart | 6 + lib/screens/filters/filters.dart | 58 +++- .../filters/update_interval_lists_modal.dart | 302 ++++++++++++++++++ lib/services/http_requests.dart | 34 ++ lib/widgets/option_box.dart | 45 +++ 8 files changed, 463 insertions(+), 10 deletions(-) create mode 100644 lib/screens/filters/update_interval_lists_modal.dart create mode 100644 lib/widgets/option_box.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 30c5f11..2eebc8f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -311,5 +311,15 @@ "enablingFiltering": "Enabling filtering...", "disablingFiltering": "Disabling filtering...", "filteringStatusUpdated": "Filtering status updated successfully", - "filteringStatusNotUpdated": "Filtering status couldn't be updated" + "filteringStatusNotUpdated": "Filtering status couldn't be updated", + "updateFrequency": "Update frequency", + "never": "Never", + "hour1": "1 hour", + "hours12": "12 hours", + "hours24": "24 hours", + "days3": "3 days", + "days7": "7 days", + "changingUpdateFrequency": "Changing...", + "updateFrequencyChanged": "Update frequency changed successfully", + "updateFrequencyNotChanged": "Updare frecuency couldn't be changed" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ece460d..1eff986 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -311,5 +311,15 @@ "enablingFiltering": "Habilitando filtrado...", "disablingFiltering": "Deshabilitando filtrado...", "filteringStatusUpdated": "Estado de filtrado actualizado correctamente", - "filteringStatusNotUpdated": "El estado de filtrado no pudo ser actualizado" + "filteringStatusNotUpdated": "El estado de filtrado no pudo ser actualizado", + "updateFrequency": "Frecuencia de actualización", + "never": "Nunca", + "hour1": "1 hora", + "hours12": "12 horas", + "hours24": "24 horas", + "days3": "3 días", + "days7": "7 días", + "changingUpdateFrequency": "Cambiando...", + "updateFrequencyChanged": "Frecuencia de actualización cambiada correctamente", + "updateFrequencyNotChanged": "La frecuencia de actualización no pudo ser cambiada" } \ No newline at end of file diff --git a/lib/models/filtering.dart b/lib/models/filtering.dart index 2bb0d39..ed5d915 100644 --- a/lib/models/filtering.dart +++ b/lib/models/filtering.dart @@ -18,8 +18,8 @@ class FilteringData { final List filters; final List whitelistFilters; List userRules; - final int interval; - final bool enabled; + int interval; + bool enabled; FilteringData({ required this.filters, diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index 6e666d0..83fcd3a 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -121,6 +121,12 @@ class ServersProvider with ChangeNotifier { void setFilteringProtectionStatus(bool status) { _serverStatus.data!.filteringEnabled = status; + _filtering.data!.enabled = status; + notifyListeners(); + } + + void setFiltersUpdateFrequency(int frequency) { + _filtering.data!.interval = frequency; notifyListeners(); } diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index 2bca389..d4f9572 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -7,6 +7,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/filters/filters_list.dart'; import 'package:adguard_home_manager/screens/filters/check_host_modal.dart'; import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; +import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; @@ -179,6 +180,37 @@ class _FiltersWidgetState extends State with TickerProviderStateM } } + void setUpdateFrequency(int value) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.changingUpdateFrequency); + + final result = await requestChangeUpdateFrequency(server: serversProvider.selectedServer!, data: { + "enabled": serversProvider.filtering.data!.enabled, + "interval": value + }); + + processModal.close(); + + if (result['result'] == 'success') { + serversProvider.setFiltersUpdateFrequency(value); + + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.updateFrequencyChanged, + color: Colors.green + ); + } + else { + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.updateFrequencyNotChanged, + color: Colors.red + ); + } + } + return DefaultTabController( length: 3, child: NestedScrollView( @@ -195,9 +227,9 @@ class _FiltersWidgetState extends State with TickerProviderStateM floating: true, forceElevated: innerBoxIsScrolled, actions: [ - IconButton( + if (serversProvider.filtering.loadStatus == 1) IconButton( onPressed: enableDisableFiltering, - tooltip: serversProvider.serverStatus.data!.filteringEnabled == true + tooltip: serversProvider.filtering.data!.enabled == true ? AppLocalizations.of(context)!.disableFiltering : AppLocalizations.of(context)!.enableFiltering, icon: Stack( @@ -214,11 +246,11 @@ class _FiltersWidgetState extends State with TickerProviderStateM color: Colors.white ), child: Icon( - serversProvider.serverStatus.data!.filteringEnabled == true + serversProvider.filtering.data!.enabled == true ? Icons.check_circle_rounded : Icons.cancel, size: 12, - color: serversProvider.serverStatus.data!.filteringEnabled == true + color: serversProvider.filtering.data!.enabled == true ? Colors.green : Colors.red, ), @@ -229,13 +261,27 @@ class _FiltersWidgetState extends State with TickerProviderStateM ], ) ), + if (serversProvider.filtering.loadStatus == 1) IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => UpdateIntervalListsModal( + interval: serversProvider.filtering.data!.interval, + onChange: setUpdateFrequency + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + }, + icon: const Icon(Icons.update_rounded) + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( onTap: fetchUpdateLists, child: Row( children: [ - const Icon(Icons.update), + const Icon(Icons.sync_rounded), const SizedBox(width: 10), Text(AppLocalizations.of(context)!.updateLists) ], @@ -253,7 +299,7 @@ class _FiltersWidgetState extends State with TickerProviderStateM ), ] ), - const SizedBox(width: 5), + const SizedBox(width: 10), ], bottom: TabBar( controller: tabController, diff --git a/lib/screens/filters/update_interval_lists_modal.dart b/lib/screens/filters/update_interval_lists_modal.dart new file mode 100644 index 0000000..4f54700 --- /dev/null +++ b/lib/screens/filters/update_interval_lists_modal.dart @@ -0,0 +1,302 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/option_box.dart'; + + +class UpdateIntervalListsModal extends StatefulWidget { + final int interval; + final void Function(int) onChange; + + const UpdateIntervalListsModal({ + Key? key, + required this.interval, + required this.onChange, + }) : super(key: key); + + @override + State createState() => _UpdateIntervalListsModalState(); +} + +class _UpdateIntervalListsModalState extends State { + int? selectedOption; + + void _updateRadioValue(value) { + setState(() { + selectedOption = value; + }); + } + + @override + void initState() { + selectedOption = widget.interval; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + + return Padding( + padding: mediaQueryData.viewInsets, + child: Container( + height: mediaQueryData.size.height > (Platform.isIOS ? 296 : 410) + ? (Platform.isIOS ? 396 : 410) + : mediaQueryData.size.height-25, + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(top: 24), + child: Icon( + Icons.update_rounded, + size: 26, + ), + ), + Container( + padding: const EdgeInsets.all(24), + width: double.maxFinite, + child: Text( + AppLocalizations.of(context)!.updateFrequency, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 24, + ), + ), + ), + SizedBox( + width: double.maxFinite, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 10, + right: 5, + bottom: 5 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 0, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 0 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.never), + ), + ), + ), + ), + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 10, + left: 5, + bottom: 5 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 1, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 1 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.hour1), + ), + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 5, + right: 5, + bottom: 5 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 12, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 12 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.hours12), + ), + ), + ), + ), + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 5, + left: 5, + bottom: 5 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 24, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 24 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.hours24), + ), + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 5, + right: 5, + bottom: 10 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 72, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 72 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.days3), + ), + ), + ), + ), + Container( + width: (mediaQueryData.size.width-70)/2, + margin: const EdgeInsets.only( + top: 5, + left: 5, + bottom: 10 + ), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 168, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 168 + ? Theme.of(context).primaryColor + : Theme.of(context).textTheme.bodyText1!.color + ), + child: Text(AppLocalizations.of(context)!.days7), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: selectedOption != null + ? () { + Navigator.pop(context); + widget.onChange(selectedOption!); + } + : null, + style: ButtonStyle( + overlayColor: MaterialStateProperty.all( + Theme.of(context).primaryColor.withOpacity(0.1) + ), + foregroundColor: MaterialStateProperty.all( + selectedOption != null + ? Theme.of(context).primaryColor + : Colors.grey, + ), + ), + 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 b17b9df..59b4b59 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -1005,4 +1005,38 @@ Future checkHostFiltered({ else { return result; } +} + +Future requestChangeUpdateFrequency({ + required Server server, + required Map data, +}) async { + final result = await apiRequest( + urlPath: '/filtering/config', + method: 'post', + server: server, + body: data, + type: 'change_update_frequency' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'change_update_frequency', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'], + resBody: result['body'], + ) + }; + } + } + else { + return result; + } } \ No newline at end of file diff --git a/lib/widgets/option_box.dart b/lib/widgets/option_box.dart new file mode 100644 index 0000000..c14cbe3 --- /dev/null +++ b/lib/widgets/option_box.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class OptionBox extends StatelessWidget { + final Widget child; + final dynamic optionsValue; + final dynamic itemValue; + final void Function(dynamic) onTap; + + const OptionBox({ + Key? key, + required this.child, + required this.optionsValue, + required this.itemValue, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(50), + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () => onTap(itemValue), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: optionsValue == itemValue + ? Theme.of(context).primaryColor + : Colors.grey + ), + color: optionsValue == itemValue + ? Theme.of(context).primaryColor.withOpacity(0.1) + : Colors.transparent, + ), + child: child, + ), + ), + ); + } +} \ No newline at end of file