From e8bc7261f3513f628ee149d18732d3b4e3af99b7 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Feb 2025 15:41:03 +0800 Subject: [PATCH] :sparkles: Updater --- assets/translations/en-US.json | 5 +- assets/translations/zh-CN.json | 5 +- assets/translations/zh-HK.json | 6 +- assets/translations/zh-TW.json | 6 +- ios/Podfile.lock | 4 +- lib/main.dart | 9 +-- lib/providers/config.dart | 12 +++- lib/screens/home.dart | 115 +++++++++++++++++++++------------ lib/screens/settings.dart | 14 ++++ lib/widgets/updater.dart | 96 +++++++++++++++++++++++++++ pubspec.yaml | 2 +- 11 files changed, 219 insertions(+), 55 deletions(-) create mode 100644 lib/widgets/updater.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 7c43d6a..1445db5 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -718,5 +718,8 @@ "stickersNew": "New Sticker", "stickersNewDescription": "Create a new sticker belongs to this pack.", "stickersPackNew": "New Sticker Pack", - "trayMenuShow": "Show" + "trayMenuShow": "Show", + "update": "Update", + "forceUpdate": "Force Update", + "forceUpdateDescription": "Force to show the application update popup, even the new version is not available." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 472178d..e15adaf 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -716,5 +716,8 @@ "stickersNew": "新建贴图", "stickersNewDescription": "创建一个新的贴图。", "stickersPackNew": "新建贴图包", - "trayMenuShow": "显示" + "trayMenuShow": "显示", + "update": "更新", + "forceUpdate": "强制更新", + "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 460d392..cf2c494 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -715,5 +715,9 @@ "fieldStickerAttachment": "附件", "stickersNew": "新建貼圖", "stickersNewDescription": "創建一個新的貼圖。", - "stickersPackNew": "新建貼圖包" + "stickersPackNew": "新建貼圖包", + "trayMenuShow": "顯示", + "update": "更新", + "forceUpdate": "強制更新", + "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index aef54cd..b3e8ccf 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -715,5 +715,9 @@ "fieldStickerAttachment": "附件", "stickersNew": "新建貼圖", "stickersNewDescription": "創建一個新的貼圖。", - "stickersPackNew": "新建貼圖包" + "stickersPackNew": "新建貼圖包", + "trayMenuShow": "顯示", + "update": "更新", + "forceUpdate": "強制更新", + "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ae56cf0..d3db8ec 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -179,7 +179,7 @@ PODS: - in_app_review (2.0.0): - Flutter - Kingfisher (8.2.0) - - livekit_client (2.3.6): + - livekit_client (2.4.0): - Flutter - flutter_webrtc - WebRTC-SDK (= 125.6422.06) @@ -426,7 +426,7 @@ SPEC CHECKSUMS: image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d - livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81 + livekit_client: 9819ebc8be8ef00ed0fae7d806bf8938ec689573 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e diff --git a/lib/main.dart b/lib/main.dart index 7f5b55a..12d5994 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -254,10 +254,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { receiveTimeout: const Duration(seconds: 60), ), ).get( - 'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1', + 'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest', ); - final remoteVersionString = - (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0'; + final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0'; final remoteVersion = Version.parse(remoteVersionString.split('+').first); final localVersion = Version.parse(localVersionString.split('+').first); final remoteBuildNumber = @@ -269,10 +268,12 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { remoteBuildNumber > localBuildNumber) && mounted) { final config = context.read(); - config.setUpdate(remoteVersionString); + config.setUpdate( + remoteVersionString, resp.data?['body'] ?? 'No changelog'); log("[Update] Update available: $remoteVersionString"); } } catch (e) { + log('[Error] Unable to check update: $e'); if (mounted) context.showErrorDialog('Unable to check update: $e'); } } diff --git a/lib/providers/config.dart b/lib/providers/config.dart index 87c6f22..1158668 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -58,7 +58,8 @@ class ConfigProvider extends ChangeNotifier { : false; } - if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) { + if (newDrawerIsExpanded != drawerIsExpanded || + newDrawerIsCollapsed != drawerIsCollapsed) { drawerIsExpanded = newDrawerIsExpanded; drawerIsCollapsed = newDrawerIsCollapsed; notifyListeners(); @@ -66,7 +67,9 @@ class ConfigProvider extends ChangeNotifier { } FilterQuality get imageQuality { - return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; + return kImageQualityLevel.values + .elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? + FilterQuality.high; } String get serverUrl { @@ -76,6 +79,7 @@ class ConfigProvider extends ChangeNotifier { bool get realmCompactView { return prefs.getBool(kAppRealmCompactView) ?? false; } + set realmCompactView(bool value) { prefs.setBool(kAppRealmCompactView, value); } @@ -86,9 +90,11 @@ class ConfigProvider extends ChangeNotifier { } String? updatableVersion; + String? updatableChangelog; - void setUpdate(String newVersion) { + void setUpdate(String newVersion, String newChangelog) { updatableVersion = newVersion; + updatableChangelog = newChangelog; notifyListeners(); } } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index b7d0c35..5886f44 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,10 +1,8 @@ -import 'dart:io'; import 'dart:math' as math; import 'dart:ui'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_app_update/flutter_app_update.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; @@ -29,6 +27,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; +import 'package:surface/widgets/updater.dart'; class HomeScreenDashEntry { final String name; @@ -83,14 +82,20 @@ class _HomeScreenState extends State { body: LayoutBuilder( builder: (context, constraints) { return Align( - alignment: constraints.maxWidth > 640 ? Alignment.center : Alignment.topCenter, + alignment: constraints.maxWidth > 640 + ? Alignment.center + : Alignment.topCenter, child: Container( constraints: const BoxConstraints(maxWidth: 640), child: SingleChildScrollView( child: Column( - mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: constraints.maxWidth > 640 + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ - _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), + _HomeDashUpdateWidget( + padding: const EdgeInsets.only( + bottom: 8, left: 8, right: 8)), _HomeDashSpecialDayWidget().padding(horizontal: 8), StaggeredGrid.extent( maxCrossAxisExtent: 280, @@ -136,21 +141,15 @@ class _HomeDashUpdateWidget extends StatelessWidget { leading: Icon(Symbols.update), title: Text('updateAvailable').tr(), subtitle: Text(config.updatableVersion!), - trailing: (kIsWeb || Platform.isWindows || Platform.isLinux) - ? null - : IconButton( - icon: const Icon(Symbols.arrow_right_alt), - onPressed: () { - final model = UpdateModel( - 'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk', - 'solian-app-release-${config.updatableVersion!}.apk', - 'ic_launcher', - 'https://apps.apple.com/us/app/solian/id6499032345', - ); - AzhonAppUpdate.update(model); - context.showSnackbar('updateOngoing'.tr()); - }, - ), + trailing: IconButton( + icon: const Icon(Symbols.arrow_right_alt), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => VersionUpdatePopup(), + ); + }, + ), ), ), ); @@ -166,7 +165,8 @@ class _HomeDashSpecialDayWidget extends StatefulWidget { const _HomeDashSpecialDayWidget(); @override - State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState(); + State<_HomeDashSpecialDayWidget> createState() => + _HomeDashSpecialDayWidgetState(); } class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> { @@ -208,7 +208,9 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> { margin: EdgeInsets.zero, child: ListTile( leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), - title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]), + title: Text('pending$name').tr(args: [ + RelativeTime(context).format(date).replaceFirst('in', '').trim() + ]), subtitle: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -297,12 +299,19 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { children: [ Text( _article!.title, - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18), - maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 18), + maxLines: + MediaQuery.of(context).size.width >= 640 ? 2 : 1, overflow: TextOverflow.ellipsis, ), Text( - parse(_article!.description).children.map((e) => e.text.trim()).join(), + parse(_article!.description) + .children + .map((e) => e.text.trim()) + .join(), maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, @@ -313,9 +322,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { crossAxisAlignment: CrossAxisAlignment.center, spacing: 2, children: [ - Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), - Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), - Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), + Text(DateFormat().format(date)).textStyle( + Theme.of(context).textTheme.bodySmall!), + Text(' · ') + .textStyle(Theme.of(context).textTheme.bodySmall!) + .bold(), + Text(RelativeTime(context).format(date)).textStyle( + Theme.of(context).textTheme.bodySmall!), ], ).opacity(0.75); }), @@ -386,15 +399,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { } Widget _buildDetailChunk(int index, bool positive) { - final prefix = positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint'; - final mod = positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount; + final prefix = + positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint'; + final mod = + positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount; final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( prefix.tr(args: ['$prefix$pos'.tr()]), - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), ), Text( '$prefix${pos}Description', @@ -429,7 +447,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { else Text( 'dailyCheckEverythingIsNegative', - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), ).tr(), const Gap(8), if (_todayRecord?.resultTier != 4) @@ -445,7 +466,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { else Text( 'dailyCheckEverythingIsPositive', - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), ).tr(), ], ), @@ -571,10 +595,12 @@ class _HomeDashNotificationWidget extends StatefulWidget { const _HomeDashNotificationWidget(); @override - State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState(); + State<_HomeDashNotificationWidget> createState() => + _HomeDashNotificationWidgetState(); } -class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> { +class _HomeDashNotificationWidgetState + extends State<_HomeDashNotificationWidget> { int? _count; Future _fetchNotificationCount() async { @@ -612,7 +638,9 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget style: Theme.of(context).textTheme.titleLarge, ).tr(), Text( - _count == null ? 'loading'.tr() : 'notificationUnreadCount'.plural(_count ?? 0), + _count == null + ? 'loading'.tr() + : 'notificationUnreadCount'.plural(_count ?? 0), style: Theme.of(context).textTheme.bodyLarge, ), ], @@ -643,10 +671,12 @@ class _HomeDashRecommendationPostWidget extends StatefulWidget { const _HomeDashRecommendationPostWidget(); @override - State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState(); + State<_HomeDashRecommendationPostWidget> createState() => + _HomeDashRecommendationPostWidgetState(); } -class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> { +class _HomeDashRecommendationPostWidgetState + extends State<_HomeDashRecommendationPostWidget> { bool _isBusy = false; List? _posts; @@ -710,13 +740,15 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati ).tr(), ], ), - Text('${_currentPage + 1}/${_posts?.length ?? 0}', style: GoogleFonts.robotoMono()) + Text('${_currentPage + 1}/${_posts?.length ?? 0}', + style: GoogleFonts.robotoMono()) ], ).padding(horizontal: 18, top: 12, bottom: 8), Expanded( child: PageView.builder( controller: _pageController, - scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: { + scrollBehavior: + ScrollConfiguration.of(context).copyWith(dragDevices: { PointerDeviceKind.mouse, PointerDeviceKind.touch, }), @@ -729,7 +761,8 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati showMenu: false, ).padding(bottom: 8), onTap: () { - GoRouter.of(context).pushNamed('postDetail', pathParameters: { + GoRouter.of(context) + .pushNamed('postDetail', pathParameters: { 'slug': _posts![index].id.toString(), }); }, diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index b628358..a3e9aec 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -24,6 +24,7 @@ import 'package:surface/providers/theme.dart'; import 'package:surface/theme.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:surface/widgets/updater.dart'; const Map kColorSchemes = { 'colorSchemeIndigo': Colors.indigo, @@ -604,6 +605,19 @@ class _SettingsScreenState extends State { } }, ), + ListTile( + title: Text('forceUpdate').tr(), + subtitle: Text('forceUpdateDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.update), + trailing: const Icon(Symbols.chevron_right), + onTap: () async { + showModalBottomSheet( + context: context, + builder: (context) => VersionUpdatePopup(), + ); + }, + ), ListTile( title: Text('settingsMiscAbout').tr(), subtitle: Text('settingsMiscAboutDescription').tr(), diff --git a/lib/widgets/updater.dart b/lib/widgets/updater.dart new file mode 100644 index 0000000..13760ae --- /dev/null +++ b/lib/widgets/updater.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_app_update/azhon_app_update.dart'; +import 'package:flutter_app_update/update_model.dart'; +import 'package:gap/gap.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/config.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/markdown_content.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class VersionUpdatePopup extends StatelessWidget { + const VersionUpdatePopup({super.key}); + + void _update(BuildContext context) async { + if (kIsWeb) return; + + final config = context.read(); + + if (Platform.isAndroid) { + final model = UpdateModel( + 'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk', + 'solian-app-release-${config.updatableVersion!}.apk', + 'ic_launcher', + 'https://apps.apple.com/us/app/solian/id6499032345', + ); + AzhonAppUpdate.update(model); + context.showSnackbar('updateOngoing'.tr()); + return; + } + + final resp = await Dio( + BaseOptions( + sendTimeout: const Duration(seconds: 60), + receiveTimeout: const Duration(seconds: 60), + ), + ).get( + 'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest', + ); + + launchUrlString(resp.data?['html_url']); + } + + @override + Widget build(BuildContext context) { + final config = context.watch(); + + return Column( + children: [ + Row( + children: [ + const Icon(Icons.update), + const Gap(16), + Text('update') + .tr() + .textStyle(Theme.of(context).textTheme.titleLarge!), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + Row( + children: [ + Expanded( + child: Text( + config.updatableVersion ?? 'unknown'.tr(), + style: GoogleFonts.robotoMono(), + ), + ), + ElevatedButton( + style: ButtonStyle( + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -3, + ), + ), + onPressed: () => _update(context), + child: Text('update').tr(), + ), + ], + ).padding(horizontal: 20), + const Divider(height: 1).padding(vertical: 8), + Expanded( + child: SingleChildScrollView( + child: MarkdownTextContent( + content: config.updatableChangelog ?? 'No changelog', + ).padding(horizontal: 20), + ), + ) + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2966358..743741b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.3.2+71 +version: 2.3.2+70 environment: sdk: ^3.5.4