From 39fb4d474f249e3269311f092d2f3bf96bbf77da Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 21 Dec 2024 23:26:42 +0800 Subject: [PATCH] :sparkles: App updates & web deeplink --- android/app/src/main/AndroidManifest.xml | 16 --- assets/translations/en-US.json | 6 +- assets/translations/zh-CN.json | 6 +- lib/main.dart | 63 ++++++++- lib/providers/config.dart | 10 +- lib/screens/home.dart | 124 ++++++++++++------ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 36 ++++- pubspec.yaml | 3 + web/.well-known/apple-app-site-association | 25 ++++ 10 files changed, 227 insertions(+), 64 deletions(-) create mode 100644 web/.well-known/apple-app-site-association diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f218860..77ad16c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,22 +33,6 @@ - - - - - - - - - - - diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 4772d77..98ce23e 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -370,6 +370,8 @@ "dailyCheckNegativeHint6": "Going out", "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", "happyBirthday": "Happy birthday, {}!", + "celebrateMerryXmas": "Merry christmas, {}!", + "celebrateNewYear": "Happy new year, {}!", "friendNew": "Add Friend", "friendRequests": "Friend Requests", "friendRequestsDescription": { @@ -455,5 +457,7 @@ "poweredBy": "Powered by {}", "shareIntent": "Share", "shareIntentDescription": "What do you want to do with the content you are sharing?", - "shareIntentPostStory": "Post a Story" + "shareIntentPostStory": "Post a Story", + "updateAvailable": "Update Available", + "updateOngoing": "正在更新,请稍后..." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1684cb7..5379d86 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -368,6 +368,8 @@ "dailyCheckNegativeHint6": "出门", "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", "happyBirthday": "生日快乐,{}!", + "celebrateMerryXmas": "圣诞快乐,{}!", + "celebrateNewYear": "新年快乐,{}!", "friendNew": "添加好友", "friendRequests": "好友请求", "friendRequestsDescription": { @@ -453,5 +455,7 @@ "poweredBy": "由 {} 提供支持", "shareIntent": "分享", "shareIntentDescription": "您想对您分享的内容做些什么?", - "shareIntentPostStory": "发布动态" + "shareIntentPostStory": "发布动态", + "updateAvailable": "检测到更新可用", + "updateOngoing": "正在更新,请稍后……" } diff --git a/lib/main.dart b/lib/main.dart index 29ea782..6ebf57d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:croppy/croppy.dart'; +import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -12,9 +13,11 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; import 'package:responsive_framework/responsive_framework.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/firebase_options.dart'; import 'package:surface/providers/channel.dart'; @@ -38,7 +41,9 @@ import 'package:surface/types/realm.dart'; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/version_label.dart'; +import 'package:version/version.dart'; import 'package:workmanager/workmanager.dart'; +import 'package:in_app_review/in_app_review.dart'; @pragma('vm:entry-point') void appBackgroundDispatcher() { @@ -125,7 +130,7 @@ class SolianApp extends StatelessWidget { Provider(create: (ctx) => HomeWidgetProvider(ctx)), // Preferences layer - Provider(create: (ctx) => ConfigProvider(ctx)), + ChangeNotifierProvider(create: (ctx) => ConfigProvider(ctx)), // Display layer ChangeNotifierProvider(create: (_) => ThemeProvider()), @@ -201,6 +206,56 @@ class _AppSplashScreen extends StatefulWidget { class _AppSplashScreenState extends State<_AppSplashScreen> { bool _isReady = false; + void _tryRequestRating() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.containsKey('first_boot_time')) { + final rawTime = prefs.getString('first_boot_time'); + final time = DateTime.tryParse(rawTime ?? ''); + if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) { + final inAppReview = InAppReview.instance; + if (prefs.getBool('rating_requested') == true) return; + if (await inAppReview.isAvailable()) { + await inAppReview.requestReview(); + prefs.setBool('rating_requested', true); + } else { + log('Unable request app review, unavailable'); + } + } + } else { + prefs.setString('first_boot_time', DateTime.now().toIso8601String()); + } + } + + Future _checkForUpdate() async { + if (kIsWeb) return; + try { + final info = await PackageInfo.fromPlatform(); + final localVersionString = '${info.version}+${info.buildNumber}'; + final resp = await Dio( + BaseOptions( + sendTimeout: const Duration(seconds: 60), + receiveTimeout: const Duration(seconds: 60), + ), + ).get( + 'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1', + ); + final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0'; + final remoteVersion = Version.parse(remoteVersionString.split('+').first); + final localVersion = Version.parse(localVersionString.split('+').first); + final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0; + final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0; + log("[Update] Local: $localVersionString, Remote: $remoteVersionString"); + // TODO remove this true + if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) { + final config = context.read(); + config.setUpdate(remoteVersionString); + log("[Update] Update available: $remoteVersionString"); + } + } catch (e) { + if (mounted) context.showErrorDialog('Unable to check update: $e'); + } + } + Future _initialize() async { try { final home = context.read(); @@ -235,7 +290,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { @override void initState() { super.initState(); - _initialize().then((_) => _postInitialization()); + _initialize().then((_) { + _postInitialization(); + _tryRequestRating(); + _checkForUpdate(); + }); } @override diff --git a/lib/providers/config.dart b/lib/providers/config.dart index 551e703..b7f70da 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -16,7 +16,7 @@ const Map kImageQualityLevel = { 'settingsImageQualityHigh': FilterQuality.high, }; -class ConfigProvider { +class ConfigProvider extends ChangeNotifier { late final SharedPreferences prefs; late final HomeWidgetProvider _home; @@ -36,8 +36,16 @@ class ConfigProvider { String get serverUrl { return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; } + set serverUrl(String url) { prefs.setString(kNetworkServerStoreKey, url); _home.saveWidgetData("nex_server_url", url); } + + String? updatableVersion; + + void setUpdate(String newVersion) { + updatableVersion = newVersion; + notifyListeners(); + } } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 671fdc6..c1a5408 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,7 +1,10 @@ +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'; @@ -10,6 +13,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter/material.dart'; +import 'package:surface/providers/config.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; @@ -69,18 +73,15 @@ 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: [ - _HomeDashSpecialDayWidget().padding(top: 8, horizontal: 8), + _HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8), + _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), StaggeredGrid.extent( maxCrossAxisExtent: 280, mainAxisSpacing: 8, @@ -104,6 +105,52 @@ class _HomeScreenState extends State { } } +class _HomeDashUpdateWidget extends StatelessWidget { + final EdgeInsets? padding; + + const _HomeDashUpdateWidget({super.key, this.padding}); + + @override + Widget build(BuildContext context) { + final config = context.watch(); + + return ListenableBuilder( + listenable: config, + builder: (context, _) { + if (config.updatableVersion != null) { + return Container( + padding: padding, + child: Card( + child: ListTile( + 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_notification', + 'https://apps.apple.com/us/app/solian/id6499032345', + ); + AzhonAppUpdate.update(model); + context.showSnackbar('updateOngoing'.tr()); + }, + ), + ), + ), + ); + } + + return SizedBox.shrink(); + }, + ); + } +} + class _HomeDashSpecialDayWidget extends StatelessWidget { const _HomeDashSpecialDayWidget({super.key}); @@ -112,10 +159,10 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { final ua = context.watch(); final today = DateTime.now(); final birthday = ua.user?.profile?.birthday?.toLocal(); - final isBirthday = birthday != null && - birthday.day == today.day && - birthday.month == today.month; + final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month; + return Column( + spacing: 8, children: [ if (isBirthday) Card( @@ -124,6 +171,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), ), ).padding(bottom: 8), + if (today.month == 12 && today.day == 25) + Card( + child: ListTile( + leading: Text('🎄').fontSize(24), + title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']), + ), + ), + if (today.month == 1 && today.day == 1) + Card( + child: ListTile( + leading: Text('🎉').fontSize(24), + title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']), + ), + ), ], ); } @@ -174,20 +235,15 @@ 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', @@ -222,10 +278,7 @@ 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) @@ -241,10 +294,7 @@ 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(), ], ), @@ -362,12 +412,10 @@ class _HomeDashNotificationWidget extends StatefulWidget { const _HomeDashNotificationWidget({super.key}); @override - State<_HomeDashNotificationWidget> createState() => - _HomeDashNotificationWidgetState(); + State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState(); } -class _HomeDashNotificationWidgetState - extends State<_HomeDashNotificationWidget> { +class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> { int? _count; Future _fetchNotificationCount() async { @@ -404,9 +452,7 @@ class _HomeDashNotificationWidgetState 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, ), ], @@ -437,12 +483,10 @@ class _HomeDashRecommendationPostWidget extends StatefulWidget { const _HomeDashRecommendationPostWidget({super.key}); @override - State<_HomeDashRecommendationPostWidget> createState() => - _HomeDashRecommendationPostWidgetState(); + State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState(); } -class _HomeDashRecommendationPostWidgetState - extends State<_HomeDashRecommendationPostWidget> { +class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> { bool _isBusy = false; List? _posts; @@ -491,8 +535,7 @@ class _HomeDashRecommendationPostWidgetState ).padding(horizontal: 18, top: 12, bottom: 8), Expanded( child: PageView.builder( - scrollBehavior: - ScrollConfiguration.of(context).copyWith(dragDevices: { + scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: { PointerDeviceKind.mouse, PointerDeviceKind.touch, }), @@ -505,8 +548,7 @@ class _HomeDashRecommendationPostWidgetState 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/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 90c93f7..463e510 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,6 +16,7 @@ import firebase_messaging import flutter_udid import flutter_webrtc import gal +import in_app_review import livekit_client import media_kit_libs_macos_video import media_kit_video @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) + InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 58bb7b0..248ea5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -627,6 +627,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + flutter_app_update: + dependency: "direct main" + description: + name: flutter_app_update + sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65" + url: "https://pub.dev" + source: hosted + version: "3.2.2" flutter_cache_manager: dependency: transitive description: @@ -729,10 +737,10 @@ packages: dependency: "direct main" description: name: flutter_udid - sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" + sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_web_plugins: dependency: "direct main" description: flutter @@ -962,6 +970,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + in_app_review: + dependency: "direct main" + description: + name: in_app_review + sha256: "36a06771b88fb0e79985b15e7f2ac0f1142e903fe72517f3c055d78bc3bc1819" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + in_app_review_platform_interface: + dependency: transitive + description: + name: in_app_review_platform_interface + sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10 + url: "https://pub.dev" + source: hosted + version: "2.0.5" intl: dependency: "direct main" description: @@ -1975,6 +1999,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + version: + dependency: "direct main" + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" very_good_infinite_list: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fde005..59d6d99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -108,6 +108,9 @@ dependencies: home_widget: ^0.7.0 receive_sharing_intent: ^1.8.1 workmanager: ^0.5.2 + flutter_app_update: ^3.2.2 + in_app_review: ^2.0.10 + version: ^3.0.2 dev_dependencies: flutter_test: diff --git a/web/.well-known/apple-app-site-association b/web/.well-known/apple-app-site-association new file mode 100644 index 0000000..6f88c26 --- /dev/null +++ b/web/.well-known/apple-app-site-association @@ -0,0 +1,25 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appIDs": [ + "W7HPZ53V6B.dev.solsynth.solian" + ], + "paths": [ + "*" + ], + "components": [ + { + "/": "/*" + } + ] + } + ] + }, + "webcredentials": { + "apps": [ + "W7HPZ53V6B.dev.solsynth.solian" + ] + } +} \ No newline at end of file