diff --git a/lib/screens/home/combined_chart.dart b/lib/screens/home/combined_chart.dart index f781b7d..89519db 100644 --- a/lib/screens/home/combined_chart.dart +++ b/lib/screens/home/combined_chart.dart @@ -104,51 +104,6 @@ class CombinedHomeChart extends StatelessWidget { ) , ); - Widget legend({ - required String label, - required Color color, - required String primaryValue, - String? secondaryValue - }) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Text( - label, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: color - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - primaryValue, - style: TextStyle( - color: color, - fontSize: 16, - fontWeight: FontWeight.w500 - ), - ), - if (secondaryValue != null) Text( - secondaryValue, - style: TextStyle( - fontSize: 10, - color: color - ), - ) - ], - ) - ], - ); - } - final hoursInterval = statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1; List dateTimes = []; @@ -193,28 +148,28 @@ class CombinedHomeChart extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - legend( + _Legend( label: data.totalQueries.label, color: data.totalQueries.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", ), const SizedBox(height: 16), - if (data.blockedFilters != null) legend( + if (data.blockedFilters != null) _Legend( label: data.blockedFilters!.label, color: data.blockedFilters!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", ), const SizedBox(height: 16), - if (data.replacedSafeBrowsing != null) legend( + if (data.replacedSafeBrowsing != null) _Legend( label: data.replacedSafeBrowsing!.label, color: data.replacedSafeBrowsing!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", ), const SizedBox(height: 16), - if (data.replacedParental != null) legend( + if (data.replacedParental != null) _Legend( label: data.replacedParental!.label, color: data.replacedParental!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), @@ -259,28 +214,28 @@ class CombinedHomeChart extends StatelessWidget { ), ), const SizedBox(height: 16), - legend( + _Legend( label: data.totalQueries.label, color: data.totalQueries.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", ), const SizedBox(height: 16), - if (data.blockedFilters != null) legend( + if (data.blockedFilters != null) _Legend( label: data.blockedFilters!.label, color: data.blockedFilters!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", ), const SizedBox(height: 16), - if (data.replacedSafeBrowsing != null) legend( + if (data.replacedSafeBrowsing != null) _Legend( label: data.replacedSafeBrowsing!.label, color: data.replacedSafeBrowsing!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", ), const SizedBox(height: 16), - if (data.replacedParental != null) legend( + if (data.replacedParental != null) _Legend( label: data.replacedParental!.label, color: data.replacedParental!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), @@ -302,4 +257,60 @@ class CombinedHomeChart extends StatelessWidget { return const SizedBox(); } } +} + +class _Legend extends StatelessWidget { + final String label; + final Color color; + final String primaryValue; + final String? secondaryValue; + + const _Legend({ + Key? key, + required this.label, + required this.color, + required this.primaryValue, + this.secondaryValue + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: color + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + primaryValue, + style: TextStyle( + color: color, + fontSize: 16, + fontWeight: FontWeight.w500 + ), + ), + if (secondaryValue != null) Text( + secondaryValue!, + style: TextStyle( + fontSize: 10, + color: color + ), + ) + ], + ) + ], + ); + } } \ No newline at end of file diff --git a/lib/screens/home/fab.dart b/lib/screens/home/fab.dart index d0701cc..bd77be7 100644 --- a/lib/screens/home/fab.dart +++ b/lib/screens/home/fab.dart @@ -1,7 +1,7 @@ import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; -import 'package:adguard_home_manager/screens/home/management_modal.dart'; +import 'package:adguard_home_manager/screens/home/management_modal/management_modal.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/constants/enums.dart'; diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index fcac0cc..293eef3 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -61,124 +61,6 @@ class _HomeState extends State { final width = MediaQuery.of(context).size.width; - Widget loading() { - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - } - - Widget loadError() { - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.errorLoadServerStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - } - - List listItems() { - return [ - ServerStatusWidget(serverStatus: statusProvider.serverStatus!), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ), - ), - const SizedBox(height: 16), - - if (appConfigProvider.combinedChartHome == false) Wrap( - children: [ - FractionallySizedBox( - widthFactor: width > 700 ? 0.5 : 1, - child: HomeChart( - data: statusProvider.serverStatus!.stats.dnsQueries, - label: AppLocalizations.of(context)!.dnsQueries, - primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), - secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", - color: Colors.blue, - hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, - ), - ), - FractionallySizedBox( - widthFactor: width > 700 ? 0.5 : 1, - child: HomeChart( - data: statusProvider.serverStatus!.stats.blockedFiltering, - label: AppLocalizations.of(context)!.blockedFilters, - primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), - secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.red, - hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, - ), - ), - FractionallySizedBox( - widthFactor: width > 700 ? 0.5 : 1, - child: HomeChart( - data: statusProvider.serverStatus!.stats.replacedSafebrowsing, - label: AppLocalizations.of(context)!.malwarePhishingBlocked, - primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), - secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.green, - hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, - ), - ), - FractionallySizedBox( - widthFactor: width > 700 ? 0.5 : 1, - child: HomeChart( - data: statusProvider.serverStatus!.stats.replacedParental, - label: AppLocalizations.of(context)!.blockedAdultWebsites, - primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), - secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedParental/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.orange, - hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, - ), - ), - ], - ), - - if (appConfigProvider.combinedChartHome == true) const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: CombinedHomeChart(), - ), - - TopItemsLists(order: appConfigProvider.homeTopItemsOrder), - ]; - } - return Scaffold( body: SafeArea( top: false, @@ -215,13 +97,119 @@ class _HomeState extends State { handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), if (statusProvider.loadStatus == LoadStatus.loading) SliverFillRemaining( - child: loading(), + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) ), if (statusProvider.loadStatus == LoadStatus.loaded) SliverList.list( - children: listItems() + children: [ + ServerStatusWidget(serverStatus: statusProvider.serverStatus!), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + ), + const SizedBox(height: 16), + + if (appConfigProvider.combinedChartHome == false) Wrap( + children: [ + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.dnsQueries, + label: AppLocalizations.of(context)!.dnsQueries, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), + secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", + color: Colors.blue, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.blockedFiltering, + label: AppLocalizations.of(context)!.blockedFilters, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.red, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.replacedSafebrowsing, + label: AppLocalizations.of(context)!.malwarePhishingBlocked, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.green, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.replacedParental, + label: AppLocalizations.of(context)!.blockedAdultWebsites, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedParental/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.orange, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + ), + ), + ], + ), + + if (appConfigProvider.combinedChartHome == true) const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: CombinedHomeChart(), + ), + + TopItemsLists(order: appConfigProvider.homeTopItemsOrder), + ], ), if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining( - child: loadError(), + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.errorLoadServerStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) ), ], ) diff --git a/lib/screens/home/management_modal.dart b/lib/screens/home/management_modal.dart deleted file mode 100644 index 9b94b58..0000000 --- a/lib/screens/home/management_modal.dart +++ /dev/null @@ -1,476 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:async'; -import 'dart:io'; - -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/functions/snackbar.dart'; -import 'package:adguard_home_manager/functions/compare_versions.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/functions/format_time.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; - -class ManagementModal extends StatefulWidget { - final bool dialog; - - const ManagementModal({ - Key? key, - required this.dialog - }) : super(key: key); - - @override - State createState() => _ManagementModalState(); -} - -class _ManagementModalState extends State with SingleTickerProviderStateMixin { - late AnimationController animationController; - late Animation animation; - final ExpandableController expandableController = ExpandableController(); - - final _chipsScrollController = ScrollController(); - - @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() { - animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void updateBlocking({ - required bool value, - required String filter, - int? time - }) async { - final result = await statusProvider.updateBlocking( - block: filter, - newStatus: value, - time: time - ); - if (mounted && result != null) { - if (result != false) { - appConfigProvider.addLog(result); - } - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.invalidUsernamePassword, - color: Colors.red - ); - } - } - - void disableWithCountdown(int time) async { - updateBlocking(value: false, filter: 'general', time: time); - expandableController.toggle(); - } - - Widget mainSwitch() { - Widget topRow({required bool legacyMode}) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (legacyMode == false) ...[ - RotationTransition( - turns: animation, - child: Icon( - Icons.keyboard_arrow_down_rounded, - size: 26, - color: statusProvider.serverStatus!.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 (statusProvider.serverStatus!.timeGeneralDisabled > 0) ...[ - const SizedBox(height: 2), - if (statusProvider.currentDeadline != null) Text( - "${AppLocalizations.of(context)!.remainingTime}: ${formatRemainingSeconds(statusProvider.remainingTime)}" - ) - ] - ], - ), - ], - ), - Switch( - value: statusProvider.serverStatus!.generalEnabled, - onChanged: statusProvider.protectionsManagementProcess.contains('general') == false - ? (value) { - if (value == false && expandableController.expanded == true && legacyMode == false) { - expandableController.toggle(); - } - updateBlocking( - value: value, - filter: legacyMode == true ? 'general_legacy' : 'general' - ); - } : null, - ) - ] - ); - } - - Widget bottomRow() { - return Container( - height: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 50 : 40, - margin: const EdgeInsets.only(top: 8), - child: Scrollbar( - controller: _chipsScrollController, - thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows, - interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows, - thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0, - child: Padding( - padding: EdgeInsets.only( - bottom: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 16 : 0 - ), - child: ListView( - controller: _chipsScrollController, - scrollDirection: Axis.horizontal, - children: [ - ActionChip( - label: Text(AppLocalizations.of(context)!.seconds(30)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => disableWithCountdown(29000) - : null, - ), - const SizedBox(width: 8), - ActionChip( - label: Text(AppLocalizations.of(context)!.minute(1)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => disableWithCountdown(59000) - : null, - ), - const SizedBox(width: 8), - ActionChip( - label: Text(AppLocalizations.of(context)!.minutes(10)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => disableWithCountdown(599000) - : null, - ), - const SizedBox(width: 8), - ActionChip( - label: Text(AppLocalizations.of(context)!.hour(1)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => disableWithCountdown(3599000) - : null, - ), - const SizedBox(width: 8), - ActionChip( - label: Text(AppLocalizations.of(context)!.hours(24)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => disableWithCountdown(86399000) - : null, - ), - ], - ), - ), - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? ExpandableNotifier( - controller: expandableController, - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: statusProvider.serverStatus!.generalEnabled == true && !statusProvider.protectionsManagementProcess.contains('general') - ? () => 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).colorScheme.primary.withOpacity(0.1) - ), - child: Expandable( - theme: const ExpandableThemeData( - animationDuration: Duration(milliseconds: 200), - fadeCurve: Curves.ease - ), - collapsed: topRow(legacyMode: false), - expanded: Column( - children: [ - topRow(legacyMode: false), - bottomRow(), - const SizedBox(height: 8) - ], - ) - ), - ), - ), - ) - ) - : Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: statusProvider.protectionsManagementProcess.contains('general') == false - ? () => updateBlocking( - value: !statusProvider.serverStatus!.generalEnabled, - filter: 'general_legacy' - ) : 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: topRow(legacyMode: true) - ), - ), - ) - ); - } - - Widget smallSwitch(String label, IconData icon, bool value, Function(bool) onChange, bool disabled) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: disabled == false - ? () => onChange(!value) - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 44, - vertical: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 16), - Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - Switch( - value: value, - onChanged: disabled == false - ? onChange - : null, - ) - ], - ), - ), - ), - ); - } - - Widget header() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Text( - AppLocalizations.of(context)!.manageServer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - ], - ); - } - - List toggles() { - return [ - mainSwitch(), - Container(height: 10), - smallSwitch( - AppLocalizations.of(context)!.ruleFiltering, - Icons.filter_list_rounded, - statusProvider.serverStatus!.filteringEnabled, - (value) => updateBlocking(value: value, filter: 'filtering'), - statusProvider.protectionsManagementProcess.contains('filtering') - ), - smallSwitch( - AppLocalizations.of(context)!.safeBrowsing, - Icons.vpn_lock_rounded, - statusProvider.serverStatus!.safeBrowsingEnabled, - (value) => updateBlocking(value: value, filter: 'safeBrowsing'), - statusProvider.protectionsManagementProcess.contains('safeBrowsing') - ), - smallSwitch( - AppLocalizations.of(context)!.parentalFiltering, - Icons.block, - statusProvider.serverStatus!.parentalControlEnabled, - (value) => updateBlocking(value: value, filter: 'parentalControl'), - statusProvider.protectionsManagementProcess.contains('parentalControl') - ), - smallSwitch( - AppLocalizations.of(context)!.safeSearch, - Icons.search_rounded, - statusProvider.serverStatus!.safeSearchEnabled, - (value) => updateBlocking(value: value, filter: 'safeSearch'), - statusProvider.protectionsManagementProcess.contains('safeSearch') - ), - ]; - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - header(), - ...toggles() - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ), - ), - ); - } - else { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - header(), - ...toggles() - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ), - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/home/management_modal/main_switch.dart b/lib/screens/home/management_modal/main_switch.dart new file mode 100644 index 0000000..6c7b2ae --- /dev/null +++ b/lib/screens/home/management_modal/main_switch.dart @@ -0,0 +1,266 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:expandable/expandable.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/functions/format_time.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +class MainSwitch extends StatelessWidget { + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final void Function(int) disableWithCountdown; + final Animation animation; + + const MainSwitch({ + Key? key, + required this.expandableController, + required this.updateBlocking, + required this.disableWithCountdown, + required this.animation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: serverVersionIsAhead( + currentVersion: statusProvider.serverStatus!.serverVersion, + referenceVersion: 'v0.107.28', + referenceVersionBeta: 'v0.108.0-b.33' + ) == true + ? ExpandableNotifier( + controller: expandableController, + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: statusProvider.serverStatus!.generalEnabled == true && !statusProvider.protectionsManagementProcess.contains('general') + ? () => 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).colorScheme.primary.withOpacity(0.1) + ), + child: Expandable( + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 200), + fadeCurve: Curves.ease + ), + collapsed: _TopRow( + legacyMode: false, + expandableController: expandableController, + updateBlocking: updateBlocking, + animation: animation, + ), + expanded: Column( + children: [ + _TopRow( + legacyMode: false, + expandableController: expandableController, + updateBlocking: updateBlocking, + animation: animation, + ), + _BottomRow( + disableWithCountdown: disableWithCountdown, + ), + const SizedBox(height: 8) + ], + ) + ), + ), + ), + ) + ) + : Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: statusProvider.protectionsManagementProcess.contains('general') == false + ? () => updateBlocking( + value: !statusProvider.serverStatus!.generalEnabled, + filter: 'general_legacy' + ) : 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: _TopRow( + legacyMode: true, + expandableController: expandableController, + updateBlocking: updateBlocking, + animation: animation, + ) + ), + ), + ) + ); + } +} + +class _TopRow extends StatelessWidget { + final bool legacyMode; + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final Animation animation; + + const _TopRow({ + Key? key, + required this.legacyMode, + required this.expandableController, + required this.updateBlocking, + required this.animation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (legacyMode == false) ...[ + RotationTransition( + turns: animation, + child: Icon( + Icons.keyboard_arrow_down_rounded, + size: 26, + color: statusProvider.serverStatus!.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 (statusProvider.serverStatus!.timeGeneralDisabled > 0) ...[ + const SizedBox(height: 2), + if (statusProvider.currentDeadline != null) Text( + "${AppLocalizations.of(context)!.remainingTime}: ${formatRemainingSeconds(statusProvider.remainingTime)}" + ) + ] + ], + ), + ], + ), + Switch( + value: statusProvider.serverStatus!.generalEnabled, + onChanged: statusProvider.protectionsManagementProcess.contains('general') == false + ? (value) { + if (value == false && expandableController.expanded == true && legacyMode == false) { + expandableController.toggle(); + } + updateBlocking( + value: value, + filter: legacyMode == true ? 'general_legacy' : 'general' + ); + } : null, + ) + ] + ); + } +} + +class _BottomRow extends StatefulWidget { + final void Function(int) disableWithCountdown; + + const _BottomRow({ + Key? key, + required this.disableWithCountdown, + }) : super(key: key); + + @override + State<_BottomRow> createState() => _BottomRowState(); +} + +class _BottomRowState extends State<_BottomRow> { + final _chipsScrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Container( + height: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 50 : 40, + margin: const EdgeInsets.only(top: 8), + child: Scrollbar( + controller: _chipsScrollController, + thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows, + interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows, + thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0, + child: Padding( + padding: EdgeInsets.only( + bottom: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 16 : 0 + ), + child: ListView( + controller: _chipsScrollController, + scrollDirection: Axis.horizontal, + children: [ + ActionChip( + label: Text(AppLocalizations.of(context)!.seconds(30)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(29000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minute(1)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(59000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minutes(10)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hour(1)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(3599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hours(24)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(86399000) + : null, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/management_modal/management_modal.dart b/lib/screens/home/management_modal/management_modal.dart new file mode 100644 index 0000000..8fa968c --- /dev/null +++ b/lib/screens/home/management_modal/management_modal.dart @@ -0,0 +1,269 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:io'; + +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/screens/home/management_modal/main_switch.dart'; +import 'package:adguard_home_manager/screens/home/management_modal/small_switch.dart'; + +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class ManagementModal extends StatefulWidget { + final bool dialog; + + const ManagementModal({ + Key? key, + required this.dialog + }) : super(key: key); + + @override + State createState() => _ManagementModalState(); +} + +class _ManagementModalState extends State with SingleTickerProviderStateMixin { + late AnimationController animationController; + late Animation animation; + final ExpandableController expandableController = ExpandableController(); + + @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() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void updateBlocking({ + required bool value, + required String filter, + int? time + }) async { + final result = await statusProvider.updateBlocking( + block: filter, + newStatus: value, + time: time + ); + if (mounted && result != null) { + if (result != false) { + appConfigProvider.addLog(result); + } + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.invalidUsernamePassword, + color: Colors.red + ); + } + } + + void disableWithCountdown(int time) async { + updateBlocking(value: false, filter: 'general', time: time); + expandableController.toggle(); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: _Modal( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ) + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ), + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: _Modal( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ) + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ), + ); + } + } +} + +class _Modal extends StatelessWidget { + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final void Function(int) disableWithCountdown; + final Animation animation; + + const _Modal({ + Key? key, + required this.expandableController, + required this.updateBlocking, + required this.disableWithCountdown, + required this.animation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Wrap( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + AppLocalizations.of(context)!.manageServer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + + MainSwitch( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ), + Container(height: 10), + SmallSwitch( + label: AppLocalizations.of(context)!.ruleFiltering, + icon: Icons.filter_list_rounded, + value: statusProvider.serverStatus!.filteringEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'filtering'), + disabled: statusProvider.protectionsManagementProcess.contains('filtering') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.safeBrowsing, + icon: Icons.vpn_lock_rounded, + value: statusProvider.serverStatus!.safeBrowsingEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'safeBrowsing'), + disabled: statusProvider.protectionsManagementProcess.contains('safeBrowsing') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.parentalFiltering, + icon: Icons.block, + value: statusProvider.serverStatus!.parentalControlEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'parentalControl'), + disabled: statusProvider.protectionsManagementProcess.contains('parentalControl') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.safeSearch, + icon: Icons.search_rounded, + value: statusProvider.serverStatus!.safeSearchEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'safeSearch'), + disabled: statusProvider.protectionsManagementProcess.contains('safeSearch') + ), + ], + ); + } +} diff --git a/lib/screens/home/management_modal/small_switch.dart b/lib/screens/home/management_modal/small_switch.dart new file mode 100644 index 0000000..4e34d68 --- /dev/null +++ b/lib/screens/home/management_modal/small_switch.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class SmallSwitch extends StatelessWidget { + final String label; + final IconData icon; + final bool value; + final void Function(bool) onChange; + final bool disabled; + + const SmallSwitch({ + Key? key, + required this.label, + required this.icon, + required this.value, + required this.onChange, + required this.disabled, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: disabled == false + ? () => onChange(!value) + : null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 44, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + icon, + size: 24, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + Switch( + value: value, + onChanged: disabled == false + ? onChange + : null, + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file