diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 87b374c..61b7e13 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -1,3 +1,5 @@ +import 'package:solian/models/account.dart'; + class Attachment { int id; DateTime createdAt; @@ -13,7 +15,8 @@ class Attachment { String destination; Map? metadata; bool isMature; - int accountId; + Account? account; + int? accountId; Attachment({ required this.id, @@ -30,6 +33,7 @@ class Attachment { required this.destination, required this.metadata, required this.isMature, + required this.account, required this.accountId, }); @@ -48,6 +52,7 @@ class Attachment { destination: json['destination'], metadata: json['metadata'], isMature: json['is_mature'], + account: json['account'] != null ? Account.fromJson(json['account']) : null, accountId: json['account_id'], ); @@ -66,6 +71,7 @@ class Attachment { 'destination': destination, 'metadata': metadata, 'is_mature': isMature, + 'account': account?.toJson(), 'account_id': accountId, }; } \ No newline at end of file diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 6992cfc..622be6b 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/request/request.dart'; -import 'package:mutex/mutex.dart'; import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/providers/websocket.dart'; import 'package:solian/services.dart'; @@ -55,7 +54,6 @@ class AuthProvider extends GetConnect { static const storage = FlutterSecureStorage(); TokenSet? credentials; - Mutex credentialsRefreshMutex = Mutex(); @override void onInit() { @@ -66,9 +64,17 @@ class AuthProvider extends GetConnect { }); } + Completer? _refreshCompleter; + Future refreshCredentials() async { + if (_refreshCompleter != null) { + await _refreshCompleter!.future; + return; + } else { + _refreshCompleter = Completer(); + } + try { - credentialsRefreshMutex.acquire(); if (!credentials!.isExpired) return; final resp = await post('/auth/token', { 'refresh_token': credentials!.refreshToken, @@ -86,10 +92,13 @@ class AuthProvider extends GetConnect { key: 'auth_credentials', value: jsonEncode(credentials!.toJson()), ); - } catch (_) { + _refreshCompleter!.complete(); + log('Refreshed credentials at ${DateTime.now()}'); + } catch (e) { + _refreshCompleter!.completeError(e); rethrow; } finally { - credentialsRefreshMutex.release(); + _refreshCompleter = null; } } @@ -124,7 +133,6 @@ class AuthProvider extends GetConnect { if (credentials!.isExpired) { await refreshCredentials(); - log('Refreshed credentials at ${DateTime.now()}'); } } diff --git a/lib/providers/relation.dart b/lib/providers/relation.dart index 930594d..b4748e5 100644 --- a/lib/providers/relation.dart +++ b/lib/providers/relation.dart @@ -18,7 +18,7 @@ class RelationshipProvider extends GetxController { bool hasFriend(Account account) { final auth = Get.find(); if (auth.userProfile.value!['id'] == account.id) return true; - return _friends.any((x) => x.id == account.id); + return _friends.any((x) => x.relatedId == account.id); } Future listRelation() { diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index 2aebe60..9400582 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -132,6 +132,7 @@ const messagesEnglish = { 'reactAdd': 'React', 'reactCompleted': 'Your reaction has been added', 'reactUncompleted': 'Your reaction has been removed', + 'attachmentUploadBy': 'Upload by', 'attachmentAdd': 'Attach attachments', 'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryVideo': 'Gallery video', diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index 97852f4..e930c2c 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -81,7 +81,7 @@ const simplifiedChineseMessages = { 'notifyAllRead': '已读所有通知', 'notifyEmpty': '通知箱为空', 'notifyEmptyCaption': '看起来最近没发生什么呢', - 'totalSocialCreditPoints': '社会信用点 async', + 'totalSocialCreditPoints': '社会信用点', 'totalPostCount': '总帖数', 'totalUpvote': '获顶数', 'totalDownvote': '获踩数', @@ -121,6 +121,7 @@ const simplifiedChineseMessages = { 'reactAdd': '作出反应', 'reactCompleted': '你的反应已被添加', 'reactUncompleted': '你的反应已被移除', + 'attachmentUploadBy': '由上传', 'attachmentAdd': '附加附件', 'attachmentAddGalleryPhoto': '相册照片', 'attachmentAddGalleryVideo': '相册视频', diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index b481e96..8c3c940 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -2,6 +2,7 @@ import 'dart:math' show min; import 'dart:ui'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/models/attachment.dart'; @@ -236,16 +237,10 @@ class AttachmentListEntry extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.close, size: 32), - const SizedBox(height: 8), - Text( - 'attachmentLoadFailed'.tr, - style: - const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - Text( - 'attachmentLoadFailedCaption'.tr, - textAlign: TextAlign.center, + Icon( + Icons.close, + size: 32, + color: Theme.of(context).colorScheme.onSurface, ), ], ), @@ -325,13 +320,12 @@ class AttachmentListEntry extends StatelessWidget { if (!showMature && item!.isMature) { onReveal(true); } else if (['image'].contains(item!.mimetype.split('/').first)) { - Navigator.of(context, rootNavigator: true).push( - MaterialPageRoute( - builder: (context) => AttachmentListFullScreen( - parentId: parentId, - attachment: item!, - ), + context.pushTransparentRoute( + AttachmentListFullScreen( + parentId: parentId, + attachment: item!, ), + rootNavigator: true, ); } }, diff --git a/lib/widgets/attachments/attachment_list_fullscreen.dart b/lib/widgets/attachments/attachment_list_fullscreen.dart index 1f2d164..311c635 100644 --- a/lib/widgets/attachments/attachment_list_fullscreen.dart +++ b/lib/widgets/attachments/attachment_list_fullscreen.dart @@ -1,5 +1,11 @@ +import 'dart:math' as math; + +import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:get/get.dart'; import 'package:solian/models/attachment.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/attachments/attachment_item.dart'; class AttachmentListFullScreen extends StatefulWidget { @@ -15,6 +21,30 @@ class AttachmentListFullScreen extends StatefulWidget { } class _AttachmentListFullScreenState extends State { + bool _showDetails = true; + + Color get _unFocusColor => + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + + String _formatBytes(int bytes, {int decimals = 2}) { + if (bytes == 0) return '0 Bytes'; + const k = 1024; + final dm = decimals < 0 ? 0 : decimals; + final sizes = [ + 'Bytes', + 'KiB', + 'MiB', + 'GiB', + 'TiB', + 'PiB', + 'EiB', + 'ZiB', + 'YiB' + ]; + final i = (math.log(bytes) / math.log(k)).floor().toInt(); + return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; + } + @override void initState() { super.initState(); @@ -22,28 +52,131 @@ class _AttachmentListFullScreenState extends State { @override Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.surface, + return DismissiblePage( + key: Key('attachment-dismissible${widget.attachment.id}'), + direction: DismissiblePageDismissDirection.multi, + onDismissed: () => Navigator.pop(context), + dismissThresholds: const { + DismissiblePageDismissDirection.multi: 0.05, + }, child: GestureDetector( - child: SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: InteractiveViewer( - boundaryMargin: const EdgeInsets.all(128), - minScale: 0.1, - maxScale: 16, - panEnabled: true, - scaleEnabled: true, - child: AttachmentItem( - parentId: widget.parentId, - showHideButton: false, - item: widget.attachment, - fit: BoxFit.contain, + child: Stack( + fit: StackFit.loose, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: InteractiveViewer( + boundaryMargin: EdgeInsets.zero, + minScale: 1, + maxScale: 16, + panEnabled: false, + scaleEnabled: true, + child: AttachmentItem( + parentId: widget.parentId, + showHideButton: false, + item: widget.attachment, + fit: BoxFit.contain, + ), + ), ), - ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 300, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Color(0xFFFFFFFF), Color(0x00FFFFFF)], + ), + ), + ), + ) + .animate(target: _showDetails ? 1 : 0) + .fadeIn(curve: Curves.fastEaseInToSlowEaseOut), + Positioned( + bottom: MediaQuery.of(context).padding.bottom, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.attachment.account != null) + Row( + children: [ + AccountAvatar( + content: widget.attachment.account!.avatar, + radius: 19, + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'attachmentUploadBy'.tr, + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + widget.attachment.account!.nick, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + ), + const SizedBox(height: 4), + Text( + widget.attachment.alt, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Wrap( + spacing: 6, + children: [ + if (widget.attachment.metadata?['width'] != null && + widget.attachment.metadata?['height'] != null) + Text( + '${widget.attachment.metadata?['width']}x${widget.attachment.metadata?['height']}', + style: TextStyle( + fontSize: 12, + color: _unFocusColor, + ), + ), + if (widget.attachment.metadata?['ratio'] != null) + Text( + '${(widget.attachment.metadata?['ratio'] as double).toPrecision(2)}', + style: TextStyle( + fontSize: 12, + color: _unFocusColor, + ), + ), + Text( + _formatBytes(widget.attachment.size), + style: TextStyle( + fontSize: 12, + color: _unFocusColor, + ), + ) + ], + ), + ], + ), + ), + ) + .animate(target: _showDetails ? 1 : 0) + .fadeIn(curve: Curves.fastEaseInToSlowEaseOut), + ], ), onTap: () { - Navigator.pop(context); + setState(() => _showDetails = !_showDetails); }, ), ); diff --git a/lib/widgets/attachments/attachment_publish.dart b/lib/widgets/attachments/attachment_publish.dart index 0c600d5..4bc24bb 100644 --- a/lib/widgets/attachments/attachment_publish.dart +++ b/lib/widgets/attachments/attachment_publish.dart @@ -191,7 +191,7 @@ class _AttachmentPublishPopupState extends State { } } - String formatBytes(int bytes, {int decimals = 2}) { + String _formatBytes(int bytes, {int decimals = 2}) { if (bytes == 0) return '0 Bytes'; const k = 1024; final dm = decimals < 0 ? 0 : decimals; @@ -353,7 +353,7 @@ class _AttachmentPublishPopupState extends State { fontFamily: 'monospace'), ), Text( - '${fileType[0].toUpperCase()}${fileType.substring(1)} · ${formatBytes(element.size)}', + '${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}', style: const TextStyle(fontSize: 12), ), diff --git a/pubspec.lock b/pubspec.lock index 8c09245..21b044f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -321,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + dismissible_page: + dependency: "direct main" + description: + name: dismissible_page + sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dropdown_button2: dependency: "direct main" description: @@ -992,14 +1000,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - mutex: - dependency: "direct main" - description: - name: mutex - sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" - url: "https://pub.dev" - source: hosted - version: "3.1.0" nm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 784cb10..38bb85e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,11 +50,11 @@ dependencies: media_kit_video: ^1.2.4 media_kit_libs_video: ^1.0.4 textfield_tags: ^3.0.1 - mutex: ^3.1.0 pasteboard: ^0.2.0 desktop_drop: ^0.4.4 badges: ^3.1.2 flutter_card_swiper: ^7.0.1 + dismissible_page: ^1.0.2 dev_dependencies: flutter_test: