diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 9db98b8..400e6a9 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -407,6 +407,7 @@ "articleWrittenAt": "Written at {}", "articleEditedAt": "Edited at {}", "attachmentSaved": "Saved to album", + "attachmentSavedDesktop": "Saved to Downloads folder", "openInAlbum": "Open in album", "postAbuseReport": "Report Post", "postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.", @@ -442,5 +443,6 @@ "postImageShareReadMore": "Scan the QR code to read full post", "postImageShareAds": "Explore posts on the Solar Network", "postShare": "Share", - "postShareImage": "Share via Image" + "postShareImage": "Share via Image", + "appInitializing": "Initializing" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index a8f8a2b..351deda 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -405,6 +405,7 @@ "articleWrittenAt": "发表于 {}", "articleEditedAt": "编辑于 {}", "attachmentSaved": "已保存到相册", + "attachmentSavedDesktop": "已保存到下载目录", "openInAlbum": "在相册中打开", "postAbuseReport": "检举帖子", "postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。", @@ -440,5 +441,6 @@ "postImageShareReadMore": "扫描右侧 QRCode 查看全文", "postImageShareAds": "来 Solar Network 探索更多有趣帖子", "postShare": "分享", - "postShareImage": "分享帖图" + "postShareImage": "分享帖图", + "appInitializing": "正在初始化" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 32cc1d2..66b6b3a 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -405,6 +405,7 @@ "articleWrittenAt": "發表於 {}", "articleEditedAt": "編輯於 {}", "attachmentSaved": "已保存到相冊", + "attachmentSavedDesktop": "已保存到下載目錄", "openInAlbum": "在相冊中打開", "postAbuseReport": "檢舉帖子", "postAbuseReportDescription": "檢舉不符合我們用户協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 24ea2eb..9ce3f6b 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -405,6 +405,7 @@ "articleWrittenAt": "發表於 {}", "articleEditedAt": "編輯於 {}", "attachmentSaved": "已儲存到相簿", + "attachmentSavedDesktop": "已儲存到下載目錄", "openInAlbum": "在相簿中開啟", "postAbuseReport": "檢舉帖子", "postAbuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。", diff --git a/lib/main.dart b/lib/main.dart index 5dfb659..d61d3ac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,11 +7,13 @@ import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; 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:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; import 'package:responsive_framework/responsive_framework.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/firebase_options.dart'; import 'package:surface/providers/channel.dart'; import 'package:surface/providers/chat_call.dart'; @@ -29,6 +31,7 @@ import 'package:surface/router.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/types/realm.dart'; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; +import 'package:surface/widgets/dialog.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -94,7 +97,7 @@ class SolianApp extends StatelessWidget { ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), ], - child: AppMainContent(), + child: _AppDelegate(), ), ), breakpoints: [ @@ -106,8 +109,8 @@ class SolianApp extends StatelessWidget { } } -class AppMainContent extends StatelessWidget { - const AppMainContent({super.key}); +class _AppDelegate extends StatelessWidget { + const _AppDelegate({super.key}); @override Widget build(BuildContext context) { @@ -129,6 +132,76 @@ class AppMainContent extends StatelessWidget { ...context.localizationDelegates, ], routerConfig: appRouter, + builder: (context, child) { + return _AppSplashScreen(child: child!); + }, ); } } + +class _AppSplashScreen extends StatefulWidget { + final Widget child; + + const _AppSplashScreen({super.key, required this.child}); + + @override + State<_AppSplashScreen> createState() => _AppSplashScreenState(); +} + +class _AppSplashScreenState extends State<_AppSplashScreen> { + bool _isReady = false; + + Future _initialize() async { + try { + final sn = context.read(); + await sn.initializeUserAgent(); + if (!mounted) return; + final ua = context.read(); + await ua.initialize(); + if (!mounted) return; + final ws = context.read(); + await ws.tryConnect(); + if (!mounted) return; + final notify = context.read(); + await notify.registerPushNotifications(); + } catch (err) { + if (!mounted) return; + await context.showErrorDialog(err); + } finally { + setState(() => _isReady = true); + } + } + + @override + void initState() { + super.initState(); + _initialize(); + } + + @override + Widget build(BuildContext context) { + if (!_isReady) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Container( + constraints: const BoxConstraints(maxWidth: 180), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("assets/icon/icon.png", width: 64, height: 64), + const Gap(6), + LinearProgressIndicator( + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + ), + const Gap(20), + Text('appInitializing'.tr(), textAlign: TextAlign.center), + ], + ), + ).center(), + ); + } + + return widget.child; + } +} diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index 0d966cc..383ce92 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -16,14 +16,6 @@ class NotificationProvider extends ChangeNotifier { NotificationProvider(BuildContext context) { _sn = context.read(); _ua = context.read(); - - // Delay to wait user provider ready to use - Future.delayed(const Duration(milliseconds: 3000), () async { - if (!_ua.isAuthorized) return; - log("Registering push notifications..."); - await registerPushNotifications(); - log("Registered push notification subscriber successfully!"); - }); } Future registerPushNotifications() async { diff --git a/lib/providers/sn_network.dart b/lib/providers/sn_network.dart index 34bdfef..b8eb034 100644 --- a/lib/providers/sn_network.dart +++ b/lib/providers/sn_network.dart @@ -1,9 +1,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; +import 'package:flutter/foundation.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:synchronized/synchronized.dart'; @@ -23,6 +27,8 @@ class SnNetworkProvider { late final SharedPreferences _prefs; + String? _userAgent; + SnNetworkProvider() { client = Dio(); @@ -46,6 +52,9 @@ class SnNetworkProvider { if (atk != null) { options.headers['Authorization'] = 'Bearer $atk'; } + if (_userAgent != null) { + options.headers['User-Agent'] = _userAgent!; + } return handler.next(options); }, ), @@ -53,11 +62,39 @@ class SnNetworkProvider { SharedPreferences.getInstance().then((prefs) { _prefs = prefs; - client.options.baseUrl = - _prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; + client.options.baseUrl = _prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; }); } + Future initializeUserAgent() async { + final String platformInfo; + if (kIsWeb) { + final deviceInfo = await DeviceInfoPlugin().webBrowserInfo; + platformInfo = 'Web; ${deviceInfo.vendor}'; + } else if (Platform.isAndroid) { + final deviceInfo = await DeviceInfoPlugin().androidInfo; + platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}'; + } else if (Platform.isIOS) { + final deviceInfo = await DeviceInfoPlugin().iosInfo; + platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}'; + } else if (Platform.isMacOS) { + final deviceInfo = await DeviceInfoPlugin().macOsInfo; + platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}'; + } else if (Platform.isWindows) { + final deviceInfo = await DeviceInfoPlugin().windowsInfo; + platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; + } else if (Platform.isLinux) { + final deviceInfo = await DeviceInfoPlugin().linuxInfo; + platformInfo = 'Linux; ${deviceInfo.prettyName}'; + } else { + platformInfo = 'Unknown'; + } + + final packageInfo = await PackageInfo.fromPlatform(); + + _userAgent = 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)'; + } + final tkLock = Lock(); Completer? _refreshCompleter; diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index 26c4ba4..dbf8d69 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -19,16 +19,17 @@ class UserProvider extends ChangeNotifier { UserProvider(BuildContext context) { _sn = context.read(); + } - SharedPreferences.getInstance().then((prefs) { - final value = prefs.getString(kAtkStoreKey); - isAuthorized = value != null; - notifyListeners(); - refreshUser().then((value) { - if (value != null) { - log('Logged in as @${value.name}'); - } - }); + Future initialize() async { + final prefs = await SharedPreferences.getInstance(); + final value = prefs.getString(kAtkStoreKey); + isAuthorized = value != null; + notifyListeners(); + refreshUser().then((value) { + if (value != null) { + log('Logged in as @${value.name}'); + } }); } diff --git a/lib/providers/websocket.dart b/lib/providers/websocket.dart index 10a45a3..07a2fb8 100644 --- a/lib/providers/websocket.dart +++ b/lib/providers/websocket.dart @@ -23,16 +23,13 @@ class WebSocketProvider extends ChangeNotifier { WebSocketProvider(BuildContext context) { _sn = context.read(); _ua = context.read(); + } - // Wait for the userinfo provide initialize authorization status - Future.delayed(const Duration(milliseconds: 250), () async { - if (_ua.isAuthorized) { - log('[WebSocket] Connecting to the server...'); - await connect(); - } else { - log('[WebSocket] Unable connect to the server, unauthorized.'); - } - }); + Future tryConnect() async { + if (!_ua.isAuthorized) return; + + log('[WebSocket] Connecting to the server...'); + await connect(); } Future connect({noRetry = false}) async { diff --git a/lib/widgets/attachment/attachment_zoom.dart b/lib/widgets/attachment/attachment_zoom.dart index 38f2e61..f4952e7 100644 --- a/lib/widgets/attachment/attachment_zoom.dart +++ b/lib/widgets/attachment/attachment_zoom.dart @@ -65,10 +65,6 @@ class _AttachmentZoomViewState extends State { return; } - if (!await Gal.hasAccess(toAlbum: true)) { - if (!await Gal.requestAccess(toAlbum: true)) return; - } - setState(() => _isDownloading = true); var extName = extension(item.name); @@ -85,6 +81,9 @@ class _AttachmentZoomViewState extends State { bool isSuccess = false; try { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + if (!await Gal.hasAccess(toAlbum: true)) { + if (!await Gal.requestAccess(toAlbum: true)) return; + } await Gal.putImage(imagePath, album: 'Solar Network'); } else { await FileSaver.instance.saveFile( @@ -104,11 +103,13 @@ class _AttachmentZoomViewState extends State { if (!mounted) return; context.showSnackbar( - 'attachmentSaved'.tr(), - action: SnackBarAction( - label: 'openInAlbum'.tr(), - onPressed: () async => Gal.open(), - ), + (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? 'attachmentSaved'.tr() : 'attachmentSavedDesktop'.tr(), + action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) + ? SnackBarAction( + label: 'openInAlbum'.tr(), + onPressed: () async => Gal.open(), + ) + : null, ); } @@ -260,6 +261,7 @@ class _AttachmentZoomViewState extends State { ).padding(right: 8), ), InkWell( + borderRadius: const BorderRadius.all(Radius.circular(16)), onTap: _isDownloading ? null : () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), @@ -335,10 +337,11 @@ class _AttachmentZoomViewState extends State { '${item.size} Bytes', style: metaTextStyle, ), - Text( - '${item.metadata['width']}x${item.metadata['height']}', - style: metaTextStyle, - ), + if (item.metadata['width'] != null && item.metadata['height'] != null) + Text( + '${item.metadata['width']}x${item.metadata['height']}', + style: metaTextStyle, + ), if (item.metadata['ratio'] != null) Text( (item.metadata['ratio'] as num).toStringAsFixed(2), diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index dbb662d..6c5f0d2 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -12,13 +12,12 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/providers/sn_attachment.dart'; +import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart'; -import '../../providers/user_directory.dart'; - class ChatMessageInput extends StatefulWidget { final ChatMessageController controller; final SnChannelMember? otherMember; diff --git a/pubspec.lock b/pubspec.lock index 61357f0..fd463c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,7 +359,7 @@ packages: source: hosted version: "0.7.10" device_info_plus: - dependency: transitive + dependency: "direct main" description: name: device_info_plus sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" @@ -1558,18 +1558,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6b34105..2ceec36 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.0.1+23 +version: 2.0.1+24 environment: sdk: ^3.5.4 @@ -101,6 +101,7 @@ dependencies: screenshot: ^3.0.0 qr_flutter: ^4.1.0 file_saver: ^0.2.14 + device_info_plus: ^11.2.0 dev_dependencies: flutter_test: