From 1ff4dc2a4b179eec630655235d5b40f49383326e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 11 Nov 2024 20:06:00 +0800 Subject: [PATCH] :sparkles: Comment list --- assets/translations/en-US.json | 5 + assets/translations/zh-CN.json | 5 + ios/Podfile.lock | 33 ----- lib/screens/explore.dart | 5 +- lib/screens/post/post_detail.dart | 29 +++- lib/widgets/attachment/attachment_detail.dart | 1 + lib/widgets/attachment/attachment_item.dart | 1 + lib/widgets/post/post_comment_list.dart | 134 ++++++++++++++++++ lib/widgets/post/post_item.dart | 57 +++++--- lib/widgets/post/post_list.dart | 0 pubspec.lock | 44 +++--- 11 files changed, 233 insertions(+), 81 deletions(-) create mode 100644 lib/widgets/post/post_comment_list.dart delete mode 100644 lib/widgets/post/post_list.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index c907edd..2093070 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -91,6 +91,11 @@ "one": "{} comment", "other": "{} comments" }, + "postCommentsDetailed": { + "zero": "No comments", + "one": "{} comment", + "other": "{} comments" + }, "settingsAppearance": "Appearance", "settingsBackgroundImage": "Background Image", "settingsBackgroundImageDescription": "Set the background image that will be applied globally.", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 73741b4..9499893 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -91,6 +91,11 @@ "one": "{} 条评论", "other": "{} 条评论" }, + "postCommentsDetailed": { + "zero": "没有评论", + "one": "{} 条评论", + "other": "{} 条评论" + }, "settingsAppearance": "外观", "settingsBackgroundImage": "背景图片", "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1f5c230..374309d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -41,41 +41,18 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - - flutter_image_compress_common (1.0.0): - - Flutter - - Mantle - - SDWebImage - - SDWebImageWebPCoder - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (3.3.1): - Flutter - image_picker_ios (0.0.1): - Flutter - - libwebp (1.3.2): - - libwebp/demux (= 1.3.2) - - libwebp/mux (= 1.3.2) - - libwebp/sharpyuv (= 1.3.2) - - libwebp/webp (= 1.3.2) - - libwebp/demux (1.3.2): - - libwebp/webp - - libwebp/mux (1.3.2): - - libwebp/demux - - libwebp/sharpyuv (1.3.2) - - libwebp/webp (1.3.2): - - libwebp/sharpyuv - - Mantle (2.2.0): - - Mantle/extobjc (= 2.2.0) - - Mantle/extobjc (2.2.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) - - SDWebImageWebPCoder (0.14.6): - - libwebp (~> 1.0) - - SDWebImage/Core (~> 5.17) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -92,7 +69,6 @@ DEPENDENCIES: - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) @@ -105,10 +81,7 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - libwebp - - Mantle - SDWebImage - - SDWebImageWebPCoder - SwiftyGif EXTERNAL SOURCES: @@ -122,8 +95,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter - flutter_image_compress_common: - :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: @@ -147,15 +118,11 @@ SPEC CHECKSUMS: DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 38a7f14..a452fad 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -168,6 +168,7 @@ class _ExploreScreenState extends State { SliverInfiniteList( itemCount: _posts.length, isLoading: _isBusy, + centerLoading: true, hasReachedMax: _postCount != null && _posts.length >= _postCount!, onFetchData: _fetchPosts, itemBuilder: (context, idx) { @@ -182,8 +183,8 @@ class _ExploreScreenState extends State { }, ); }, - separatorBuilder: (context, index) => const Divider(), - ) + separatorBuilder: (context, index) => const Divider(height: 1), + ), ], ), ), diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index 32c450a..f78097a 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -11,6 +12,7 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_item.dart'; class PostDetailScreen extends StatefulWidget { @@ -89,13 +91,26 @@ class _PostDetailScreenState extends State { ], ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), ), - body: SingleChildScrollView( - child: Column( - children: [ - LoadingIndicator(isActive: _isBusy), - if (_data != null) PostItem(data: _data!), - ], - ), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: LoadingIndicator(isActive: _isBusy), + ), + if (_data != null) + SliverToBoxAdapter( + child: PostItem(data: _data!), + ), + const SliverToBoxAdapter(child: Divider(height: 1)), + if (_data != null) + SliverToBoxAdapter( + child: Text('postCommentsDetailed') + .plural(_data!.metric.replyCount) + .textStyle(Theme.of(context).textTheme.titleLarge!) + .padding(horizontal: 16, top: 12, bottom: 4), + ), + if (_data != null) PostCommentSliverList(parentPostId: _data!.id), + SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), + ], ), ); } diff --git a/lib/widgets/attachment/attachment_detail.dart b/lib/widgets/attachment/attachment_detail.dart index 0e268e4..d483fda 100644 --- a/lib/widgets/attachment/attachment_detail.dart +++ b/lib/widgets/attachment/attachment_detail.dart @@ -27,6 +27,7 @@ class AttachmentDetailPopup extends StatelessWidget { child: Hero( tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}', child: PhotoView( + key: Key('attachment-detail-${data.rid}-$heroTag'), imageProvider: UniversalImage.provider( sn.getAttachmentUrl(data.rid), ), diff --git a/lib/widgets/attachment/attachment_item.dart b/lib/widgets/attachment/attachment_item.dart index ead8491..e73df98 100644 --- a/lib/widgets/attachment/attachment_item.dart +++ b/lib/widgets/attachment/attachment_item.dart @@ -26,6 +26,7 @@ class AttachmentItem extends StatelessWidget { child: LayoutBuilder(builder: (context, constraints) { return UniversalImage( sn.getAttachmentUrl(data.rid), + key: Key('attachment-${data.rid}-$heroTag'), fit: BoxFit.cover, cacheHeight: constraints.maxHeight, cacheWidth: constraints.maxWidth, diff --git a/lib/widgets/post/post_comment_list.dart b/lib/widgets/post/post_comment_list.dart new file mode 100644 index 0000000..405851b --- /dev/null +++ b/lib/widgets/post/post_comment_list.dart @@ -0,0 +1,134 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_attachment.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/post.dart'; +import 'package:surface/widgets/post/post_item.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; + +class PostCommentSliverList extends StatefulWidget { + final int parentPostId; + const PostCommentSliverList({super.key, required this.parentPostId}); + + @override + State createState() => _PostCommentSliverListState(); +} + +class _PostCommentSliverListState extends State { + bool _isBusy = true; + + final List _posts = List.empty(growable: true); + int? _postCount; + + Future _fetchPosts() async { + if (_postCount != null && _posts.length >= _postCount!) return; + + setState(() => _isBusy = true); + + final sn = context.read(); + final resp = await sn.client.get( + '/cgi/co/posts/${widget.parentPostId}/replies', + queryParameters: { + 'take': 10, + 'offset': _posts.length, + }, + ); + final List out = + List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []); + + Set rids = {}; + for (var i = 0; i < out.length; i++) { + rids.addAll(out[i].body['attachments']?.cast() ?? []); + } + + if (!mounted) return; + final attach = context.read(); + final attachments = await attach.getMultiple(rids.toList()); + for (var i = 0; i < out.length; i++) { + out[i] = out[i].copyWith( + preload: SnPostPreload( + attachments: attachments + .where( + (ele) => out[i].body['attachments']?.contains(ele.rid) ?? false, + ) + .toList(), + ), + ); + } + + _postCount = resp.data['count']; + _posts.addAll(out); + + if (mounted) setState(() => _isBusy = false); + } + + @override + void initState() { + super.initState(); + _fetchPosts(); + } + + @override + Widget build(BuildContext context) { + return SliverInfiniteList( + itemCount: _posts.length, + isLoading: _isBusy, + hasReachedMax: _postCount != null && _posts.length >= _postCount!, + onFetchData: _fetchPosts, + itemBuilder: (context, idx) { + return GestureDetector( + child: PostItem(data: _posts[idx]), + onTap: () { + GoRouter.of(context).pushNamed( + 'postDetail', + pathParameters: {'slug': _posts[idx].id.toString()}, + extra: _posts[idx], + ); + }, + ); + }, + separatorBuilder: (context, index) => const Divider(height: 1), + ); + } +} + +class PostCommentListPopup extends StatelessWidget { + final int postId; + final int commentCount; + const PostCommentListPopup({ + super.key, + required this.postId, + this.commentCount = 0, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.comment, size: 24), + const Gap(16), + Text('postCommentsDetailed') + .plural(commentCount) + .textStyle(Theme.of(context).textTheme.titleLarge!), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + Expanded( + child: CustomScrollView( + slivers: [ + PostCommentSliverList(parentPostId: postId), + ], + ), + ), + ], + ); + } +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index de5acd2..3451f06 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -11,10 +11,16 @@ import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/markdown_content.dart'; import 'package:gap/gap.dart'; +import 'package:surface/widgets/post/post_comment_list.dart'; class PostItem extends StatelessWidget { final SnPost data; - const PostItem({super.key, required this.data}); + final bool showComments; + const PostItem({ + super.key, + required this.data, + this.showComments = true, + }); @override Widget build(BuildContext context) { @@ -24,9 +30,12 @@ class PostItem extends StatelessWidget { _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8), _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), if (data.preload?.attachments?.isNotEmpty ?? true) - AttachmentList(data: data.preload!.attachments!, bordered: true), - _PostBottomAction(data: data) - .padding(left: 20, right: 26, top: 8, bottom: 2), + AttachmentList( + data: data.preload!.attachments!, + bordered: true, + ), + _PostBottomAction(data: data, showComments: showComments) + .padding(left: 12, right: 18), ], ); } @@ -34,7 +43,8 @@ class PostItem extends StatelessWidget { class _PostBottomAction extends StatelessWidget { final SnPost data; - const _PostBottomAction({required this.data}); + final bool showComments; + const _PostBottomAction({required this.data, required this.showComments}); @override Widget build(BuildContext context) { @@ -53,25 +63,38 @@ class _PostBottomAction extends StatelessWidget { const Gap(8), Text('postReact').tr(), ], - ), + ).padding(horizontal: 8, vertical: 8), onTap: () {}, ), - const Gap(16), - InkWell( - child: Row( - children: [ - Icon(Symbols.comment, size: 20, color: iconColor), - const Gap(8), - Text('postComments').plural(data.metric.replyCount), - ], + if (showComments) + InkWell( + child: Row( + children: [ + Icon(Symbols.comment, size: 20, color: iconColor), + const Gap(8), + Text('postComments').plural(data.metric.replyCount), + ], + ).padding(horizontal: 8, vertical: 8), + onTap: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => PostCommentListPopup( + postId: data.id, + commentCount: data.metric.replyCount, + ), + ); + }, ), - onTap: () {}, - ), ].expand((ele) => [ele, const Gap(8)]).toList() ..removeLast(), ), InkWell( - child: Icon(Symbols.share, size: 20, color: iconColor), + child: Icon( + Symbols.share, + size: 20, + color: iconColor, + ).padding(horizontal: 8, vertical: 8), onTap: () {}, ), ], diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart deleted file mode 100644 index e69de29..0000000 diff --git a/pubspec.lock b/pubspec.lock index 188d160..9bab929 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" animations: dependency: "direct main" description: @@ -202,10 +202,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: transitive description: @@ -790,18 +790,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -838,10 +838,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" markdown: dependency: "direct main" description: @@ -1150,7 +1150,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1227,10 +1227,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1251,10 +1251,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" styled_widget: dependency: "direct main" description: @@ -1291,10 +1291,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: @@ -1411,10 +1411,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" watcher: dependency: transitive description: