From e02b598be99a0041f08880f986489ff694d40b9b Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Thu, 6 Apr 2023 22:56:32 +0200 Subject: [PATCH] Added screen to update server --- lib/constants/urls.dart | 1 + lib/functions/compare_versions.dart | 47 ++++ lib/functions/open_url.dart | 17 ++ lib/l10n/app_en.arb | 13 +- lib/l10n/app_es.arb | 13 +- lib/models/update_available.dart | 55 +++++ lib/providers/servers_provider.dart | 62 ++++- lib/screens/settings/settings.dart | 51 ++-- lib/screens/settings/update.dart | 350 ++++++++++++++++++++++++++++ lib/services/http_requests.dart | 168 +++++++++++++ lib/widgets/bottom_nav_bar.dart | 32 ++- pubspec.lock | 40 +++- pubspec.yaml | 4 + 13 files changed, 819 insertions(+), 34 deletions(-) create mode 100644 lib/functions/compare_versions.dart create mode 100644 lib/functions/open_url.dart create mode 100644 lib/models/update_available.dart create mode 100644 lib/screens/settings/update.dart diff --git a/lib/constants/urls.dart b/lib/constants/urls.dart index 5b60855..e37435d 100644 --- a/lib/constants/urls.dart +++ b/lib/constants/urls.dart @@ -3,4 +3,5 @@ class Urls { static const String gitHub = "https://github.com/JGeek00/adguard-home-manager"; static const String customRuleDocs = "https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters"; static const String checkLatestReleaseUrl = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases/latest"; + static const String adGuardHomeLatestRelease = "https://api.github.com/repos/AdGuardTeam/AdGuardHome/releases/latest"; } \ No newline at end of file diff --git a/lib/functions/compare_versions.dart b/lib/functions/compare_versions.dart new file mode 100644 index 0000000..58c9542 --- /dev/null +++ b/lib/functions/compare_versions.dart @@ -0,0 +1,47 @@ +bool compareVersions({ + required String currentVersion, + required String newVersion +}) { + final currentSplit = currentVersion.split('.').map((e) => int.parse(e)).toList(); + final newSplit = newVersion.split('.').map((e) => int.parse(e)).toList(); + + if (newSplit[0] > currentSplit[0]) { + return true; + } + else if (newSplit[1] > currentSplit[1]) { + return true; + } + else if (newSplit[2] > currentSplit[2]) { + return true; + } + else { + return false; + } +} + +bool compareBetaVersions({ + required String currentVersion, + required String newVersion +}) { + final currentSplit = currentVersion.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + final newSplit = newVersion.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + + final currentBeta = int.parse(currentVersion.split('-')[1].replaceAll('b.', '')); + final newBeta = int.parse(newVersion.split('-')[1].replaceAll('b.', '')); + + if (newSplit[0] > currentSplit[0]) { + return true; + } + else if (newSplit[1] > currentSplit[1]) { + return true; + } + else if (newSplit[2] > currentSplit[2]) { + return true; + } + else if (newBeta > currentBeta) { + return true; + } + else { + return false; + } +} \ No newline at end of file diff --git a/lib/functions/open_url.dart b/lib/functions/open_url.dart new file mode 100644 index 0000000..b4d52d5 --- /dev/null +++ b/lib/functions/open_url.dart @@ -0,0 +1,17 @@ +import 'package:flutter_web_browser/flutter_web_browser.dart'; + +void openUrl(String url) { + FlutterWebBrowser.openWebPage( + url: url, + customTabsOptions: const CustomTabsOptions( + instantAppsEnabled: true, + showTitle: true, + urlBarHidingEnabled: false, + ), + safariVCOptions: const SafariViewControllerOptions( + barCollapsingEnabled: true, + dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, + modalPresentationCapturesStatusBarAppearance: true, + ) + ); +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index aebf7da..088a127 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -585,5 +585,16 @@ "invalidDomain": "Invalid domain", "loadingBlockedServicesList": "Loading blocked services list...", "blockedServicesListNotLoaded": "The blocked services list could not be loaded", - "error": "Error" + "error": "Error", + "updates": "Updates", + "updatesDescription": "Update the AdGuard Home server", + "updateNow": "Update now", + "currentVersion": "Current version", + "requestStartUpdateFailed": "Request to start update failed", + "requestStartUpdateSuccessful": "Request to start update successfull", + "serverUpdated": "Server is updated", + "unknownStatus": "Unknown status", + "checkingUpdates": "Checking updates...", + "checkUpdates": "Check updates", + "requestingUpdate": "Requesting update..." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5f94cb5..026aade 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -585,5 +585,16 @@ "invalidDomain": "Dominio no válido", "loadingBlockedServicesList": "Cargando lista de servicios bloqueados...", "blockedServicesListNotLoaded": "No se ha podido cargar la lista de servicios bloqueados", - "error": "Error" + "error": "Error", + "updates": "Actualizaciones", + "updatesDescription": "Actualiza el servidor AdGuard Home", + "updateNow": "Actualizar ahora", + "currentVersion": "Versión actual", + "requestStartUpdateFailed": "Petición para iniciar la actualización fallida", + "requestStartUpdateSuccessful": "Petición para iniciar la actualización satisfactoria", + "serverUpdated": "Servidor actualizado", + "unknownStatus": "Estado desconocido", + "checkingUpdates": "Comprobando actualizaciones...", + "checkUpdates": "Comprobar actualizaciones", + "requestingUpdate": "Solicitando actualización..." } \ No newline at end of file diff --git a/lib/models/update_available.dart b/lib/models/update_available.dart new file mode 100644 index 0000000..2b2aca6 --- /dev/null +++ b/lib/models/update_available.dart @@ -0,0 +1,55 @@ +import 'package:adguard_home_manager/constants/enums.dart'; + +class UpdateAvailable { + LoadStatus loadStatus = LoadStatus.loading; + UpdateAvailableData? data; + + UpdateAvailable({ + required this.loadStatus, + required this.data + }); +} + +class UpdateAvailableData { + String currentVersion; + final String newVersion; + final String announcement; + final String announcementUrl; + final bool canAutoupdate; + final bool disabled; + String? changelog; + bool? updateAvailable; + + UpdateAvailableData({ + required this.currentVersion, + required this.newVersion, + required this.announcement, + required this.announcementUrl, + required this.canAutoupdate, + required this.disabled, + this.changelog, + this.updateAvailable + }); + + factory UpdateAvailableData.fromJson(Map json) => UpdateAvailableData( + currentVersion: json["current_version"], + newVersion: json["new_version"], + announcement: json["announcement"], + announcementUrl: json["announcement_url"], + canAutoupdate: json["can_autoupdate"], + disabled: json["disabled"], + changelog: json["changelog"], + updateAvailable: json['update_available'] + ); + + Map toJson() => { + "current_version": currentVersion, + "new_version": newVersion, + "announcement": announcement, + "announcement_url": announcementUrl, + "can_autoupdate": canAutoupdate, + "disabled": disabled, + "changelog": changelog, + "update_available": updateAvailable + }; +} diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index c35f3fa..40210a5 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -1,5 +1,3 @@ -import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/models/blocked_services.dart'; import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; @@ -9,11 +7,15 @@ import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/models/rewrite_rules.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/models/update_available.dart'; +import 'package:adguard_home_manager/models/blocked_services.dart'; 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/conversions.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; class ServersProvider with ChangeNotifier { Database? _dbInstance; @@ -56,6 +58,11 @@ class ServersProvider with ChangeNotifier { services: null ); + final UpdateAvailable _updateAvailable = UpdateAvailable( + loadStatus: LoadStatus.loading, + data: null, + ); + FilteringStatus? _filteringStatus; List get serversList { @@ -102,6 +109,10 @@ class ServersProvider with ChangeNotifier { return _blockedServicesList; } + UpdateAvailable get updateAvailable { + return _updateAvailable; + } + void setDbInstance(Database db) { _dbInstance = db; } @@ -223,6 +234,18 @@ class ServersProvider with ChangeNotifier { notifyListeners(); } } + + void setUpdateAvailableLoadStatus(LoadStatus status, bool notify) { + _updateAvailable.loadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + void setUpdateAvailableData(UpdateAvailableData data) { + _updateAvailable.data = data; + notifyListeners(); + } Future createServer(Server server) async { final saved = await saveServerIntoDb(server); @@ -460,6 +483,40 @@ class ServersProvider with ChangeNotifier { } } + void checkServerUpdatesAvailable(Server server) async { + setUpdateAvailableLoadStatus(LoadStatus.loading, true); + final result = await Future.wait([ + checkServerUpdates(server: server), + getUpdateChangelog(server: server) + ]); + if (result[0]['result'] == 'success') { + UpdateAvailableData data = result[0]['data']; + data.changelog = result[1]['body']; + data.updateAvailable = data.newVersion.contains('b') + ? compareBetaVersions( + currentVersion: data.currentVersion.replaceAll('v', ''), + newVersion: data.newVersion.replaceAll('v', ''), + ) + : compareVersions( + currentVersion: data.currentVersion.replaceAll('v', ''), + newVersion: data.newVersion.replaceAll('v', ''), + ); + setUpdateAvailableData(data); + setUpdateAvailableLoadStatus(LoadStatus.loaded, true); + } + else { + setUpdateAvailableLoadStatus(LoadStatus.error, true); + } + } + + void clearUpdateAvailable(Server server, String newCurrentVersion) { + if (_updateAvailable.data != null) { + _updateAvailable.data!.updateAvailable = null; + _updateAvailable.data!.currentVersion = newCurrentVersion; + notifyListeners(); + } + } + void saveFromDb(List>? data) async { if (data != null) { for (var server in data) { @@ -484,6 +541,7 @@ class ServersProvider with ChangeNotifier { if (serverStatus['result'] == 'success') { _serverStatus.data = serverStatus['data']; _serverStatus.loadStatus = 1; + checkServerUpdatesAvailable(serverObj); // Do not await } else { _serverStatus.loadStatus = 2; diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 21dff92..aacaa01 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart'; @@ -10,9 +9,9 @@ import 'package:adguard_home_manager/screens/settings/access_settings/access_set import 'package:adguard_home_manager/screens/settings/customization/customization.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/screens/settings/update.dart'; import 'package:adguard_home_manager/screens/settings/dns/dns.dart'; import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrites.dart'; -import 'package:adguard_home_manager/screens/settings/appbar.dart'; import 'package:adguard_home_manager/screens/servers/servers.dart'; import 'package:adguard_home_manager/screens/settings/advanced_setings.dart'; import 'package:adguard_home_manager/screens/settings/general_settings.dart'; @@ -20,6 +19,7 @@ import 'package:adguard_home_manager/screens/settings/general_settings.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/constants/strings.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/constants/urls.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; @@ -38,23 +38,7 @@ class Settings extends StatelessWidget { MaterialPageRoute(builder: (context) => const Servers()) ); })); - } - - void openWeb(String url) { - FlutterWebBrowser.openWebPage( - url: url, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); - } + } return Scaffold( appBar: AppBar( @@ -125,6 +109,31 @@ class Settings extends StatelessWidget { ) }, ), + CustomListTile( + icon: Icons.system_update_rounded, + title: AppLocalizations.of(context)!.updates, + subtitle: AppLocalizations.of(context)!.updatesDescription, + trailing: serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.updateAvailable != null && + serversProvider.updateAvailable.data!.updateAvailable == true + ? Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ) + : null, + onTap: () => { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const UpdateScreen() + ) + ) + }, + ), CustomListTile( icon: Icons.info_rounded, title: AppLocalizations.of(context)!.serverInformation, @@ -196,7 +205,7 @@ class Settings extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( - onPressed: () => openWeb(Urls.playStore), + onPressed: () => openUrl(Urls.playStore), icon: SvgPicture.asset( 'assets/resources/google-play.svg', color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -206,7 +215,7 @@ class Settings extends StatelessWidget { tooltip: AppLocalizations.of(context)!.visitGooglePlay, ), IconButton( - onPressed: () => openWeb(Urls.gitHub), + onPressed: () => openUrl(Urls.gitHub), icon: SvgPicture.asset( 'assets/resources/github.svg', color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/screens/settings/update.dart b/lib/screens/settings/update.dart new file mode 100644 index 0000000..beba263 --- /dev/null +++ b/lib/screens/settings/update.dart @@ -0,0 +1,350 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:html/parser.dart' as html; +import 'package:markdown/markdown.dart' as md; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/functions/compare_versions.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/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class UpdateScreen extends StatelessWidget { + const UpdateScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void update() async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.requestingUpdate); + + final result = await requestUpdateServer(server: serversProvider.selectedServer!); + + processModal.close(); + + if (result['result'] == 'success') { + serversProvider.clearUpdateAvailable(serversProvider.selectedServer!, serversProvider.updateAvailable.data!.newVersion); + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateSuccessful, + color: Colors.green, + labelColor: Colors.white, + ); + } + else { + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateFailed, + color: Colors.red, + labelColor: Colors.white, + ); + appConfigProvider.addLog(result['log']); + } + } + + Widget headerPortrait() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () => Navigator.pop(context), + ), + IconButton( + icon: Icon( + Icons.refresh_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: AppLocalizations.of(context)!.checkUpdates, + onPressed: () => serversProvider.checkServerUpdatesAvailable(serversProvider.selectedServer!) + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, bottom: 16, left: 16, right: 16 + ), + child: Column( + children: [ + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? Column( + children: const [ + CircularProgressIndicator(), + SizedBox(height: 4) + ], + ) + : Icon( + serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? Icons.system_update_rounded + : Icons.system_security_update_good_rounded + : Icons.system_security_update_warning_rounded, + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? AppLocalizations.of(context)!.checkingUpdates + : serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? AppLocalizations.of(context)!.updateAvailable + : AppLocalizations.of(context)!.serverUpdated + : AppLocalizations.of(context)!.unknownStatus, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400 + ), + ), + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded) Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true + ? AppLocalizations.of(context)!.newVersion + : AppLocalizations.of(context)!.currentVersion, + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? serversProvider.updateAvailable.data!.newVersion + : serversProvider.updateAvailable.data!.currentVersion + : "N/A", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ), + if (serversProvider.updateAvailable.loadStatus != LoadStatus.loaded) const SizedBox(), + FilledButton.icon( + icon: const Icon(Icons.download_rounded), + label: Text(AppLocalizations.of(context)!.updateNow), + onPressed: serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true + ? () => update() + : null + ) + ], + ), + ], + ), + ), + ], + ); + } + + Widget headerLandscape() { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () => Navigator.pop(context), + ), + IconButton( + icon: Icon( + Icons.refresh_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: AppLocalizations.of(context)!.checkUpdates, + onPressed: () => serversProvider.checkServerUpdatesAvailable(serversProvider.selectedServer!) + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 8, bottom: 16, left: 16, right: 16 + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? Column( + children: const [ + CircularProgressIndicator(), + SizedBox(height: 4) + ], + ) + : Icon( + serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? Icons.system_update_rounded + : Icons.system_security_update_good_rounded + : Icons.system_security_update_warning_rounded, + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? AppLocalizations.of(context)!.checkingUpdates + : serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? AppLocalizations.of(context)!.updateAvailable + : AppLocalizations.of(context)!.serverUpdated + : AppLocalizations.of(context)!.unknownStatus, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400 + ), + ), + const SizedBox(height: 40), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded) Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true + ? AppLocalizations.of(context)!.newVersion + : AppLocalizations.of(context)!.currentVersion, + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + serversProvider.updateAvailable.data!.updateAvailable != null + ? serversProvider.updateAvailable.data!.updateAvailable == true + ? serversProvider.updateAvailable.data!.newVersion + : serversProvider.updateAvailable.data!.currentVersion + : "N/A", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ), + if (serversProvider.updateAvailable.loadStatus != LoadStatus.loaded) const SizedBox(), + FilledButton.icon( + icon: const Icon(Icons.download_rounded), + label: Text(AppLocalizations.of(context)!.updateNow), + onPressed: serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true + ? () => update() + : null + ) + ], + ), + ), + ], + ), + ), + ), + ], + ); + } + + final changelog = serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null + ? ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Changelog ${serversProvider.updateAvailable.data!.newVersion}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: html.parse(md.markdownToHtml(serversProvider.updateAvailable.data!.changelog!)).outerHtml, + onLinkTap: (url, context, attributes, element) => url != null ? openUrl(url) : null, + ) + ) + ], + ) + : null; + + return Scaffold( + body: MediaQuery.of(context).size.width > 700 + ? Row( + children: [ + Expanded( + flex: 2, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + height: MediaQuery.of(context).size.height, + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + ), + child: headerLandscape(), + ) + ], + ), + ), + ), + Expanded( + flex: 3, + child: SafeArea( + child: SizedBox( + width: MediaQuery.of(context).size.width*0.6, + child: changelog ?? const SizedBox(), + ), + ), + ) + ], + ) + : Column( + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: SafeArea( + child: headerPortrait() + ) + ), + changelog != null + ? Expanded(child: changelog) + : const SizedBox(), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 1647526..309ef75 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -20,6 +20,7 @@ import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/models/update_available.dart'; Future> apiRequest({ @@ -1959,4 +1960,171 @@ Future checkAppUpdatesGitHub() async { ) }; } +} + +Future checkServerUpdates({ + required Server server, +}) async { + final result = await Future.wait([ + apiRequest( + urlPath: '/version.json', + method: 'get', + server: server, + type: 'check_server_updates', + body: json.encode({ + "recheck_now": true + }) + ), + apiRequest( + urlPath: '/status', + method: 'get', + server: server, + type: 'check_server_updates', + body: json.encode({ + "recheck_now": true + }) + ), + ]); + + if (result[0]['hasResponse'] == true && result[0]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[0]['statusCode'] == 200) { + final Map obj = { + ...jsonDecode(result[0]['body']), + 'current_version': jsonDecode(result[1]['body'])['version'] + }; + return { + 'result': 'success', + 'data': UpdateAvailableData.fromJson(obj) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } +} + +Future getUpdateChangelog({ + required Server server, +}) async { + try { + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.getUrl(Uri.parse(Urls.adGuardHomeLatestRelease)); + HttpClientResponse response = await request.close(); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + if (response.statusCode == 200) { + return { + 'result': 'success', + 'hasResponse': true, + 'error': false, + 'statusCode': response.statusCode, + 'body': jsonDecode(reply)['body'] + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: response.statusCode.toString(), + resBody: reply, + ) + }; + } + } on SocketException { + return { + 'result': 'no_connection', + 'message': 'SocketException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'SocketException' + ) + }; + } on TimeoutException { + return { + 'result': 'no_connection', + 'message': 'TimeoutException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'TimeoutException' + ) + }; + } on HandshakeException { + return { + 'result': 'ssl_error', + 'message': 'HandshakeException', + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: 'HandshakeException' + ) + }; + } catch (e) { + return { + 'result': 'error', + 'message': e.toString(), + 'log': AppLog( + type: 'check_latest_release_github', + dateTime: DateTime.now(), + message: e.toString() + ) + }; + } +} + +Future requestUpdateServer({ + required Server server, +}) async { + final result = await apiRequest( + urlPath: '/update', + method: 'post', + server: server, + type: 'update_server' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_server', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } } \ No newline at end of file diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index dabc657..5793442 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -50,11 +50,33 @@ class BottomNavBar extends StatelessWidget { return NavigationBar( selectedIndex: appConfigProvider.selectedScreen, destinations: screens.map((screen) => NavigationDestination( - icon: Icon( - screen.icon, - color: screens[appConfigProvider.selectedScreen] == screen - ? Theme.of(context).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onSurfaceVariant, + icon: Stack( + children: [ + Icon( + screen.icon, + color: screens[appConfigProvider.selectedScreen] == screen + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + if ( + screen.name == 'settings' && + serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.updateAvailable != null && + serversProvider.updateAvailable.data!.updateAvailable == true + ) Positioned( + bottom: 0, + right: -12, + child: Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ), + ) + ], ), label: translatedName(screen.name) )).toList(), diff --git a/pubspec.lock b/pubspec.lock index fd32536..93d9e05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "342c7908f0a67bcec62b6e0f7cf23e23bafe7f64693665dd35be98d5e783bdfd" + url: "https://pub.dev" + source: hosted + version: "3.0.0-alpha.6" flutter_launcher_icons: dependency: "direct dev" description: @@ -267,6 +275,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f" + url: "https://pub.dev" + source: hosted + version: "0.6.14" flutter_native_splash: dependency: "direct dev" description: @@ -302,13 +318,13 @@ packages: source: sdk version: "0.0.0" html: - dependency: transitive + dependency: "direct main" description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.2" http: dependency: transitive description: @@ -365,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + markdown: + dependency: "direct main" + description: + name: markdown + sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 + url: "https://pub.dev" + source: hosted + version: "7.0.2" matcher: dependency: transitive description: @@ -397,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + numerus: + dependency: transitive + description: + name: numerus + sha256: "436759d84f233b40107d0cc31cfa92d24e0960afeb2e506be70926d4cddffd9e" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_info_plus: dependency: "direct main" description: @@ -651,5 +683,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.1 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index f5e0639..f50f1ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,10 @@ dependencies: bottom_sheet: ^3.1.2 percent_indicator: ^4.2.2 store_checker: ^1.1.0 + flutter_markdown: ^0.6.14 + markdown: ^7.0.2 + html: ^0.15.2 + flutter_html: ^3.0.0-alpha.6 dev_dependencies: flutter_test: