From 009c710485aac081897b28410e82ba677886a370 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 15:02:03 -0300 Subject: [PATCH 1/4] fix: Do not search for an update executable on mac os --- lib/providers/update_provider.dart | 5 +- lib/screens/settings/shared/update.dart | 55 +++++++++++++++------- lib/screens/settings/updates_and_help.dart | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart index 5707aa9f..b8c6768b 100644 --- a/lib/providers/update_provider.dart +++ b/lib/providers/update_provider.dart @@ -306,12 +306,15 @@ class UpdateManager extends UnityProvider { if (file.existsSync()) return file; } else { throw UnsupportedError( - 'Unsupported platform. Only Windows and Linux are supported', + 'Unsupported platform. Only Windows and Linux are supported.', ); } return null; } + String get downloadMacOSRedirect => + 'https://github.com/bluecherrydvr/unity?tab=readme-ov-file#download'; + /// Downloads the latest version executable. Future download(String version) async { assert( diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index 35f5583c..76789ac5 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +import 'dart:io'; + import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; @@ -36,20 +38,28 @@ class AppUpdateCard extends StatelessWidget { @override Widget build(BuildContext context) { + assert(!kIsWeb, 'This widget should not be used on the web'); final theme = Theme.of(context); final loc = AppLocalizations.of(context); final textDirection = Directionality.of(context); final update = context.watch(); + final horizontalPadding = + DesktopSettings.horizontalPadding.resolve(textDirection); + final cardMargin = EdgeInsetsDirectional.only( + top: 8.0, + start: horizontalPadding.left, + end: horizontalPadding.right, + bottom: 6.0, + ); + + final isMacOs = isDesktopPlatform && Platform.isMacOS; + if (update.hasUpdateAvailable) { - final executable = update.executableFor(update.latestVersion!.version); + final executable = + isMacOs ? null : update.executableFor(update.latestVersion!.version); return Card( - margin: EdgeInsetsDirectional.only( - top: 8.0, - start: DesktopSettings.horizontalPadding.resolve(textDirection).left, - end: DesktopSettings.horizontalPadding.resolve(textDirection).right, - bottom: 6.0, - ), + margin: cardMargin, child: Padding( padding: const EdgeInsetsDirectional.all(8.0), child: Row(children: [ @@ -81,7 +91,17 @@ class AppUpdateCard extends StatelessWidget { style: theme.textTheme.labelLarge, ), const SizedBox(height: 6.0), - if (update.downloading) + if (isMacOs) + Link( + uri: Uri.parse(update.downloadMacOSRedirect), + builder: (context, followLink) { + return FilledButton( + onPressed: followLink, + child: const Text('Download at website'), + ); + }, + ) + else if (update.downloading) SizedBox( height: 32.0, width: 32.0, @@ -115,12 +135,7 @@ class AppUpdateCard extends StatelessWidget { ); } else { return Card( - margin: EdgeInsetsDirectional.only( - top: 8.0, - bottom: 6.0, - start: DesktopSettings.horizontalPadding.resolve(textDirection).left, - end: DesktopSettings.horizontalPadding.resolve(textDirection).right, - ), + margin: cardMargin, child: Padding( padding: const EdgeInsetsDirectional.all(8.0), child: Row(children: [ @@ -160,14 +175,18 @@ class AppUpdateCard extends StatelessWidget { if (DateUtils.isSameDay( update.lastCheck, DateTime.now(), - )) return loc.today; + )) { + return '${loc.today}, ${DateFormat.Hms().format(update.lastCheck!)}'; + } if (DateUtils.isSameDay( update.lastCheck, DateTime.now().subtract( const Duration(days: 1, minutes: 12), ), - )) return loc.yesterday; + )) { + return '${loc.yesterday}, ${DateFormat.Hms().format(update.lastCheck!)}'; + } return DateFormat().format(update.lastCheck!); }(), @@ -178,7 +197,7 @@ class AppUpdateCard extends StatelessWidget { ), ), FilledButton.tonal( - onPressed: update.checkForUpdates, + onPressed: update.loading ? null : update.checkForUpdates, child: update.loading ? SizedBox( height: 20.0, @@ -270,7 +289,7 @@ class AppUpdateOptions extends StatelessWidget { ), isThreeLine: true, ), - if (kDebugMode) + if (kDebugMode && !Platform.isMacOS) CheckboxListTile.adaptive( secondary: CircleAvatar( backgroundColor: Colors.transparent, diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index e2fed0eb..6ba6cf6b 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -53,10 +53,10 @@ class UpdatesSettings extends StatelessWidget { ]), const AppUpdateCard(), const AppUpdateOptions(), + const Divider(), ], // TODO(bdlukaa): Show option to downlaod the native client when running // on the web. - const Divider(), const About(), ]); } From 67170192941a44707d02431b06f76c62a5fe4104 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 15:04:02 -0300 Subject: [PATCH 2/4] fix: External redirections --- lib/screens/servers/add_server_info.dart | 4 ++-- lib/screens/settings/shared/update.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/screens/servers/add_server_info.dart b/lib/screens/servers/add_server_info.dart index a7636b78..c583d1a7 100644 --- a/lib/screens/servers/add_server_info.dart +++ b/lib/screens/servers/add_server_info.dart @@ -71,7 +71,7 @@ class AddServerInfoScreen extends StatelessWidget { const SizedBox(height: 16.0), Row(mainAxisAlignment: MainAxisAlignment.end, children: [ Link( - uri: Uri.https('www.bluecherrydvr.com', '/'), + uri: Uri.https('bluecherrydvr.com', '/'), builder: (context, open) { return TextButton( onPressed: open, @@ -82,7 +82,7 @@ class AddServerInfoScreen extends StatelessWidget { const SizedBox(width: 8.0), Link( uri: Uri.https( - 'www.bluecherrydvr.com', + 'bluecherrydvr.com', '/product/v3license/', ), builder: (context, open) { diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index 76789ac5..c7c978ad 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -410,7 +410,7 @@ class About extends StatelessWidget { Row( children: [ Link( - uri: Uri.https('www.bluecherrydvr.com', '/'), + uri: Uri.https('bluecherrydvr.com', '/'), builder: (context, open) { return TextButton( onPressed: open, @@ -426,7 +426,7 @@ class About extends StatelessWidget { ), const SizedBox(width: 8.0), Link( - uri: Uri.https('www.bluecherrydvr.com', '/contact/'), + uri: Uri.https('bluecherrydvr.com', '/contact/'), builder: (context, open) { return TextButton( onPressed: open, From 94104661611c160d4728e4d29e524c7a827ce09d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 15:11:41 -0300 Subject: [PATCH 3/4] chore: Move update data to a single file --- lib/screens/settings/shared/update.dart | 468 --------------------- lib/screens/settings/updates_and_help.dart | 436 ++++++++++++++++++- 2 files changed, 433 insertions(+), 471 deletions(-) delete mode 100644 lib/screens/settings/shared/update.dart diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart deleted file mode 100644 index c7c978ad..00000000 --- a/lib/screens/settings/shared/update.dart +++ /dev/null @@ -1,468 +0,0 @@ -/* - * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). - * - * Copyright 2022 Bluecherry, LLC - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import 'dart:io'; - -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; -import 'package:bluecherry_client/utils/methods.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher/link.dart'; - -/// The card that displays the update information. -class AppUpdateCard extends StatelessWidget { - const AppUpdateCard({super.key}); - - @override - Widget build(BuildContext context) { - assert(!kIsWeb, 'This widget should not be used on the web'); - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - final textDirection = Directionality.of(context); - final update = context.watch(); - - final horizontalPadding = - DesktopSettings.horizontalPadding.resolve(textDirection); - final cardMargin = EdgeInsetsDirectional.only( - top: 8.0, - start: horizontalPadding.left, - end: horizontalPadding.right, - bottom: 6.0, - ); - - final isMacOs = isDesktopPlatform && Platform.isMacOS; - - if (update.hasUpdateAvailable) { - final executable = - isMacOs ? null : update.executableFor(update.latestVersion!.version); - return Card( - margin: cardMargin, - child: Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: Row(children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 12.0), - child: Icon( - Icons.update, - size: 54.0, - color: theme.colorScheme.primary, - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loc.newVersionAvailable, - style: theme.textTheme.titleMedium, - ), - Text(update.latestVersion!.description), - ], - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - update.latestVersion!.version, - style: theme.textTheme.labelLarge, - ), - const SizedBox(height: 6.0), - if (isMacOs) - Link( - uri: Uri.parse(update.downloadMacOSRedirect), - builder: (context, followLink) { - return FilledButton( - onPressed: followLink, - child: const Text('Download at website'), - ); - }, - ) - else if (update.downloading) - SizedBox( - height: 32.0, - width: 32.0, - child: isCupertino - ? CupertinoActivityIndicator.partiallyRevealed( - progress: update.downloadProgress, - ) - : CircularProgressIndicator( - value: update.downloadProgress, - strokeWidth: 2.0, - ), - ) - else if (executable != null) - FilledButton( - onPressed: () => update.install( - onFail: (type) => showInstallFailDialog(context, type), - ), - child: Text(loc.installVersion), - ) - else - FilledButton( - onPressed: () => update.download( - update.latestVersion!.version, - ), - child: Text(loc.downloadVersion), - ), - ], - ), - ]), - ), - ); - } else { - return Card( - margin: cardMargin, - child: Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: Row(children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 12.0), - child: Stack(alignment: AlignmentDirectional.center, children: [ - Icon( - Icons.update, - size: 54.0, - color: theme.colorScheme.primary, - ), - const PositionedDirectional( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check_circle, - color: Colors.green, - ), - ), - ), - ]), - ), - Expanded( - child: RichText( - text: TextSpan(children: [ - TextSpan( - text: '${loc.upToDate}\n', - style: theme.textTheme.headlineMedium, - ), - TextSpan( - text: loc.lastChecked( - () { - if (update.lastCheck == null) return loc.never; - if (DateUtils.isSameDay( - update.lastCheck, - DateTime.now(), - )) { - return '${loc.today}, ${DateFormat.Hms().format(update.lastCheck!)}'; - } - - if (DateUtils.isSameDay( - update.lastCheck, - DateTime.now().subtract( - const Duration(days: 1, minutes: 12), - ), - )) { - return '${loc.yesterday}, ${DateFormat.Hms().format(update.lastCheck!)}'; - } - - return DateFormat().format(update.lastCheck!); - }(), - ), - style: theme.textTheme.labelMedium, - ), - ]), - ), - ), - FilledButton.tonal( - onPressed: update.loading ? null : update.checkForUpdates, - child: update.loading - ? SizedBox( - height: 20.0, - width: 20.0, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2.0, - semanticsLabel: loc.checkingForUpdates, - ), - ) - : Text(loc.checkForUpdates), - ), - ]), - ), - ); - } - } - - void showInstallFailDialog(BuildContext context, FailType failType) { - final loc = AppLocalizations.of(context); - - showDialog( - context: context, - builder: (context) { - late String title; - late String failMessage; - - switch (failType) { - case FailType.executableNotFound: - title = loc.failedToUpdate; - failMessage = loc.executableNotFound; - break; - } - - return AlertDialog( - title: Text(title), - content: Text(failMessage), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(loc.ok), - ), - ], - ); - }, - ); - } -} - -class AppUpdateOptions extends StatelessWidget { - const AppUpdateOptions({super.key}); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - final theme = Theme.of(context); - final update = context.watch(); - return Column(children: [ - CheckboxListTile.adaptive( - onChanged: (v) { - if (v != null) { - update.automaticDownloads = v; - } - }, - value: update.automaticDownloads, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.podcasts), - ), - contentPadding: DesktopSettings.horizontalPadding, - title: Text(loc.automaticDownloadUpdates), - subtitle: RichText( - text: TextSpan( - children: [ - TextSpan(text: loc.automaticDownloadUpdatesDescription), - TextSpan( - text: '\n${loc.learnMore}', - style: theme.textTheme.labelMedium!.copyWith( - color: theme.colorScheme.primary, - ), - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ], - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface, - ), - ), - ), - isThreeLine: true, - ), - if (kDebugMode && !Platform.isMacOS) - CheckboxListTile.adaptive( - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.memory), - ), - title: const Text('Show release notes'), - subtitle: const Text( - 'Display release notes when a new version is downloaded', - ), - contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.history), - ), - contentPadding: DesktopSettings.horizontalPadding, - title: Text(loc.updateHistory), - trailing: const Icon(Icons.navigate_next), - onTap: () => showUpdateHistory(context), - ), - ]); - } - - void showUpdateHistory(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - final update = context.watch(); - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - return DraggableScrollableSheet( - expand: false, - maxChildSize: 0.8, - initialChildSize: 0.8, - builder: (context, controller) { - return ListView.builder( - controller: controller, - itemCount: update.versions.length, - itemBuilder: (context, index) { - final version = update.versions.reversed.elementAt(index); - return ListTile( - title: Row(children: [ - RichText( - text: TextSpan( - children: [ - TextSpan(text: version.version), - const TextSpan(text: ' '), - TextSpan( - text: SettingsProvider.instance.kDateFormat.value - .format( - DateFormat('EEE, d MMM yyyy', 'en_US') - .parse(version.publishedAt), - ), - style: theme.textTheme.labelSmall, - ), - ], - style: theme.textTheme.bodyMedium, - ), - ), - const Expanded( - child: Padding( - padding: EdgeInsetsDirectional.only(start: 12.0), - child: Divider(), - ), - ), - ]), - subtitle: Text(version.description), - isThreeLine: true, - trailing: Link( - uri: Uri.parse( - 'https://github.com/bluecherrydvr/unity/releases/tag/v${version.version}'), - builder: (context, followLink) { - return TextButton( - onPressed: followLink, - child: Text(loc.learnMore), - ); - }, - ), - ); - }, - ); - }, - ); - }, - ); - } -} - -class About extends StatelessWidget { - const About({super.key}); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - final theme = Theme.of(context); - final update = context.watch(); - - return Padding( - padding: DesktopSettings.horizontalPadding, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 8.0), - if (update.packageInfo != null) ...[ - Text(update.packageInfo!.version), - const SizedBox(height: 8.0), - Text( - loc.versionText, - style: theme.textTheme.displayMedium, - ), - ], - const SizedBox(height: 8.0), - Row( - children: [ - Link( - uri: Uri.https('bluecherrydvr.com', '/'), - builder: (context, open) { - return TextButton( - onPressed: open, - child: Text( - loc.website, - semanticsLabel: 'www.bluecherrydvr.com', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ); - }, - ), - const SizedBox(width: 8.0), - Link( - uri: Uri.https('bluecherrydvr.com', '/contact/'), - builder: (context, open) { - return TextButton( - onPressed: open, - child: Text( - 'Help', - semanticsLabel: 'www.bluecherrydvr.com/contact', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ); - }, - ), - const SizedBox(width: 8.0), - TextButton( - onPressed: () { - showLicensePage( - context: context, - applicationName: 'Bluecherry Client', - applicationIcon: Image.asset( - 'assets/images/icon.png', - ), - applicationVersion: update.packageInfo?.version, - applicationLegalese: '© 2022 Bluecherry, LLC', - ); - }, - child: Text( - 'Licenses', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), - ]), - ); - } -} diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index 6ba6cf6b..8bca7b01 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -17,14 +17,21 @@ * along with this program. If not, see . */ -import 'dart:io'; +import 'dart:io' hide Link; +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/screens/settings/shared/update.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; +import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/link.dart'; class UpdatesSettings extends StatelessWidget { const UpdatesSettings({super.key}); @@ -32,6 +39,8 @@ class UpdatesSettings extends StatelessWidget { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + final update = context.watch(); return ListView(children: [ if (!kIsWeb) ...[ @@ -52,7 +61,66 @@ class UpdatesSettings extends StatelessWidget { ), ]), const AppUpdateCard(), - const AppUpdateOptions(), + CheckboxListTile.adaptive( + onChanged: (v) { + if (v != null) { + update.automaticDownloads = v; + } + }, + value: update.automaticDownloads, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.podcasts), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: Text(loc.automaticDownloadUpdates), + subtitle: RichText( + text: TextSpan( + children: [ + TextSpan(text: loc.automaticDownloadUpdatesDescription), + TextSpan( + text: '\n${loc.learnMore}', + style: theme.textTheme.labelMedium!.copyWith( + color: theme.colorScheme.primary, + ), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ], + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + isThreeLine: true, + ), + if (kDebugMode && !Platform.isMacOS) + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.memory), + ), + title: const Text('Show release notes'), + subtitle: const Text( + 'Display release notes when a new version is downloaded', + ), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.history), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: Text(loc.updateHistory), + trailing: const Icon(Icons.navigate_next), + onTap: () => showUpdateHistory(context), + ), const Divider(), ], // TODO(bdlukaa): Show option to downlaod the native client when running @@ -60,4 +128,366 @@ class UpdatesSettings extends StatelessWidget { const About(), ]); } + + void showUpdateHistory(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) { + final settings = context.watch(); + final update = context.watch(); + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.8, + initialChildSize: 0.8, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: update.versions.length, + itemBuilder: (context, index) { + final version = update.versions.reversed.elementAt(index); + return ListTile( + title: Row(children: [ + RichText( + text: TextSpan( + children: [ + TextSpan(text: version.version), + const TextSpan(text: ' '), + TextSpan( + text: settings.kDateFormat.value.format( + DateFormat('EEE, d MMM yyyy', 'en_US') + .parse(version.publishedAt), + ), + style: theme.textTheme.labelSmall, + ), + ], + style: theme.textTheme.bodyMedium, + ), + ), + const Expanded( + child: Padding( + padding: EdgeInsetsDirectional.only(start: 12.0), + child: Divider(), + ), + ), + ]), + subtitle: Text(version.description), + isThreeLine: true, + trailing: Link( + uri: Uri.parse( + 'https://github.com/bluecherrydvr/unity/releases/tag/v${version.version}'), + builder: (context, followLink) { + return TextButton( + onPressed: followLink, + child: Text(loc.learnMore), + ); + }, + ), + ); + }, + ); + }, + ); + }, + ); + } +} + +/// The card that displays the update information. +class AppUpdateCard extends StatelessWidget { + const AppUpdateCard({super.key}); + + @override + Widget build(BuildContext context) { + assert(!kIsWeb, 'This widget should not be used on the web'); + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final textDirection = Directionality.of(context); + final update = context.watch(); + + final horizontalPadding = + DesktopSettings.horizontalPadding.resolve(textDirection); + final cardMargin = EdgeInsetsDirectional.only( + top: 8.0, + start: horizontalPadding.left, + end: horizontalPadding.right, + bottom: 6.0, + ); + + final isMacOs = isDesktopPlatform && Platform.isMacOS; + + if (update.hasUpdateAvailable) { + final executable = + isMacOs ? null : update.executableFor(update.latestVersion!.version); + return Card( + margin: cardMargin, + child: Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: Row(children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 12.0), + child: Icon( + Icons.update, + size: 54.0, + color: theme.colorScheme.primary, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.newVersionAvailable, + style: theme.textTheme.titleMedium, + ), + Text(update.latestVersion!.description), + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + update.latestVersion!.version, + style: theme.textTheme.labelLarge, + ), + const SizedBox(height: 6.0), + if (isMacOs) + Link( + uri: Uri.parse(update.downloadMacOSRedirect), + builder: (context, followLink) { + return FilledButton( + onPressed: followLink, + child: const Text('Download at website'), + ); + }, + ) + else if (update.downloading) + SizedBox( + height: 32.0, + width: 32.0, + child: isCupertino + ? CupertinoActivityIndicator.partiallyRevealed( + progress: update.downloadProgress, + ) + : CircularProgressIndicator( + value: update.downloadProgress, + strokeWidth: 2.0, + ), + ) + else if (executable != null) + FilledButton( + onPressed: () => update.install( + onFail: (type) => showInstallFailDialog(context, type), + ), + child: Text(loc.installVersion), + ) + else + FilledButton( + onPressed: () => update.download( + update.latestVersion!.version, + ), + child: Text(loc.downloadVersion), + ), + ], + ), + ]), + ), + ); + } else { + return Card( + margin: cardMargin, + child: Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: Row(children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 12.0), + child: Stack(alignment: AlignmentDirectional.center, children: [ + Icon( + Icons.update, + size: 54.0, + color: theme.colorScheme.primary, + ), + const PositionedDirectional( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_circle, + color: Colors.green, + ), + ), + ), + ]), + ), + Expanded( + child: RichText( + text: TextSpan(children: [ + TextSpan( + text: '${loc.upToDate}\n', + style: theme.textTheme.headlineMedium, + ), + TextSpan( + text: loc.lastChecked( + () { + if (update.lastCheck == null) return loc.never; + if (DateUtils.isSameDay( + update.lastCheck, + DateTime.now(), + )) { + return '${loc.today}, ${DateFormat.Hms().format(update.lastCheck!)}'; + } + + if (DateUtils.isSameDay( + update.lastCheck, + DateTime.now().subtract( + const Duration(days: 1, minutes: 12), + ), + )) { + return '${loc.yesterday}, ${DateFormat.Hms().format(update.lastCheck!)}'; + } + + return DateFormat().format(update.lastCheck!); + }(), + ), + style: theme.textTheme.labelMedium, + ), + ]), + ), + ), + FilledButton.tonal( + onPressed: update.loading ? null : update.checkForUpdates, + child: update.loading + ? SizedBox( + height: 20.0, + width: 20.0, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2.0, + semanticsLabel: loc.checkingForUpdates, + ), + ) + : Text(loc.checkForUpdates), + ), + ]), + ), + ); + } + } + + void showInstallFailDialog(BuildContext context, FailType failType) { + final loc = AppLocalizations.of(context); + + showDialog( + context: context, + builder: (context) { + late String title; + late String failMessage; + + switch (failType) { + case FailType.executableNotFound: + title = loc.failedToUpdate; + failMessage = loc.executableNotFound; + break; + } + + return AlertDialog( + title: Text(title), + content: Text(failMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(loc.ok), + ), + ], + ); + }, + ); + } +} + +class About extends StatelessWidget { + const About({super.key}); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + final update = context.watch(); + + return Padding( + padding: DesktopSettings.horizontalPadding, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 8.0), + if (update.packageInfo != null) ...[ + Text(update.packageInfo!.version), + const SizedBox(height: 8.0), + Text( + loc.versionText, + style: theme.textTheme.displayMedium, + ), + ], + const SizedBox(height: 8.0), + Row( + children: [ + Link( + uri: Uri.https('bluecherrydvr.com', '/'), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text( + loc.website, + semanticsLabel: 'www.bluecherrydvr.com', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ); + }, + ), + const SizedBox(width: 8.0), + Link( + uri: Uri.https('bluecherrydvr.com', '/contact/'), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text( + 'Help', + semanticsLabel: 'www.bluecherrydvr.com/contact', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ); + }, + ), + const SizedBox(width: 8.0), + TextButton( + onPressed: () { + showLicensePage( + context: context, + applicationName: 'Bluecherry Client', + applicationIcon: Image.asset( + 'assets/images/icon.png', + ), + applicationVersion: update.packageInfo?.version, + applicationLegalese: '© 2022 Bluecherry, LLC', + ); + }, + child: Text( + 'Licenses', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ), + ], + ), + ]), + ); + } } From 32cba6fa878873afa7e6eef82f868900905901fa Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 15:13:45 -0300 Subject: [PATCH 4/4] fix: Do not allow to automatically download updates on macos --- lib/screens/settings/updates_and_help.dart | 63 +++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index 8bca7b01..82cf762a 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -42,6 +42,8 @@ class UpdatesSettings extends StatelessWidget { final theme = Theme.of(context); final update = context.watch(); + final isMacOs = isDesktopPlatform && Platform.isMacOS; + return ListView(children: [ if (!kIsWeb) ...[ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -61,41 +63,42 @@ class UpdatesSettings extends StatelessWidget { ), ]), const AppUpdateCard(), - CheckboxListTile.adaptive( - onChanged: (v) { - if (v != null) { - update.automaticDownloads = v; - } - }, - value: update.automaticDownloads, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.podcasts), - ), - contentPadding: DesktopSettings.horizontalPadding, - title: Text(loc.automaticDownloadUpdates), - subtitle: RichText( - text: TextSpan( - children: [ - TextSpan(text: loc.automaticDownloadUpdatesDescription), - TextSpan( - text: '\n${loc.learnMore}', - style: theme.textTheme.labelMedium!.copyWith( - color: theme.colorScheme.primary, + if (!isMacOs) + CheckboxListTile.adaptive( + onChanged: (v) { + if (v != null) { + update.automaticDownloads = v; + } + }, + value: update.automaticDownloads, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.podcasts), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: Text(loc.automaticDownloadUpdates), + subtitle: RichText( + text: TextSpan( + children: [ + TextSpan(text: loc.automaticDownloadUpdatesDescription), + TextSpan( + text: '\n${loc.learnMore}', + style: theme.textTheme.labelMedium!.copyWith( + color: theme.colorScheme.primary, + ), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer()..onTap = () {}, ), - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer()..onTap = () {}, + ], + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface, ), - ], - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface, ), ), + isThreeLine: true, ), - isThreeLine: true, - ), - if (kDebugMode && !Platform.isMacOS) + if (kDebugMode && !isMacOs) CheckboxListTile.adaptive( secondary: CircleAvatar( backgroundColor: Colors.transparent,