From 7b96013406ed6705a3b93b204678e50af029acb6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Mar 2025 21:41:38 +0800 Subject: [PATCH] :sparkles: Better(?) comment nesting --- assets/translations/en-US.json | 1 + assets/translations/zh-CN.json | 1 + assets/translations/zh-HK.json | 1 + assets/translations/zh-TW.json | 1 + lib/widgets/account/account_image.dart | 3 + lib/widgets/post/post_comment_list.dart | 2 + lib/widgets/post/post_item.dart | 131 ++++++++++++++++++++++-- 7 files changed, 132 insertions(+), 8 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index a761ffd..a6d339d 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -207,6 +207,7 @@ "one": "{} comment", "other": "{} comments" }, + "postCommentExpand": "Show comments", "settingsAppearance": "Appearance", "settingsCustomFonts": "Custom Fonts", "settingsCustomFontsDescription": "Set custom fonts for the application.", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index ea639c2..aabea29 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -205,6 +205,7 @@ "one": "{} 条评论", "other": "{} 条评论" }, + "postCommentExpand": "展开评论", "settingsAppearance": "外观", "settingsCustomFonts": "自定义字体", "settingsCustomFontsDescription": "设置应用程序使用的字体。", diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index d47a093..41b403e 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -205,6 +205,7 @@ "one": "{} 條評論", "other": "{} 條評論" }, + "postCommentExpand": "展開評論", "settingsAppearance": "外觀", "settingsCustomFonts": "自定義字體", "settingsCustomFontsDescription": "設置應用程序使用的字體。", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 3b66708..e2f1daf 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -205,6 +205,7 @@ "one": "{} 條評論", "other": "{} 條評論" }, + "postCommentExpand": "展開評論", "settingsAppearance": "外觀", "settingsCustomFonts": "自定義字體", "settingsCustomFontsDescription": "設置應用程序使用的字體。", diff --git a/lib/widgets/account/account_image.dart b/lib/widgets/account/account_image.dart index 568c7a5..d6bad1d 100644 --- a/lib/widgets/account/account_image.dart +++ b/lib/widgets/account/account_image.dart @@ -14,6 +14,7 @@ class AccountImage extends StatelessWidget { final Widget? fallbackWidget; final Widget? badge; final Offset? badgeOffset; + final FilterQuality? filterQuality; const AccountImage({ super.key, @@ -25,6 +26,7 @@ class AccountImage extends StatelessWidget { this.fallbackWidget, this.badge, this.badgeOffset, + this.filterQuality, }); @override @@ -54,6 +56,7 @@ class AccountImage extends StatelessWidget { ) : AutoResizeUniversalImage( sn.getAttachmentUrl(url), + filterQuality: filterQuality, key: Key('attachment-${content.hashCode}'), fit: BoxFit.cover, ), diff --git a/lib/widgets/post/post_comment_list.dart b/lib/widgets/post/post_comment_list.dart index a22b1cd..32bf9a3 100644 --- a/lib/widgets/post/post_comment_list.dart +++ b/lib/widgets/post/post_comment_list.dart @@ -138,6 +138,7 @@ class PostCommentSliverListState extends State { child: PostItem( data: _posts[idx], maxWidth: widget.maxWidth, + showExpandableComments: true, onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null, @@ -209,6 +210,7 @@ class _PostCommentListPopupState extends State { if (ua.isAuthorized) SliverToBoxAdapter( child: Container( + margin: const EdgeInsets.only(bottom: 8), height: 240, decoration: BoxDecoration( border: Border.symmetric( diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 4ef8ba6..e4cd8fd 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -50,6 +50,7 @@ class OpenablePostItem extends StatelessWidget { final bool showComments; final bool showMenu; final bool showFullPost; + final bool showExpandableComments; final double? maxWidth; final Function(SnPost data)? onChanged; final Function()? onDeleted; @@ -62,6 +63,7 @@ class OpenablePostItem extends StatelessWidget { this.showComments = true, this.showMenu = true, this.showFullPost = false, + this.showExpandableComments = false, this.maxWidth, this.onChanged, this.onDeleted, @@ -83,6 +85,7 @@ class OpenablePostItem extends StatelessWidget { maxWidth: maxWidth, showComments: showComments, showFullPost: showFullPost, + showExpandableComments: showExpandableComments, onChanged: onChanged, onDeleted: onDeleted, onSelectAnswer: onSelectAnswer, @@ -115,6 +118,7 @@ class PostItem extends StatelessWidget { final bool showComments; final bool showMenu; final bool showFullPost; + final bool showExpandableComments; final double? maxWidth; final Function(SnPost data)? onChanged; final Function()? onDeleted; @@ -127,6 +131,7 @@ class PostItem extends StatelessWidget { this.showComments = true, this.showMenu = true, this.showFullPost = false, + this.showExpandableComments = false, this.maxWidth, this.onChanged, this.onDeleted, @@ -354,14 +359,17 @@ class PostItem extends StatelessWidget { ), if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!) - .padding(horizontal: 12, vertical: 4), + .padding(left: 60, right: 12, top: 12, bottom: 4), if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) LinkPreviewWidget( text: data.body['content'], ).padding(left: 60, right: 4), - _PostFeaturedComment(data: data, maxWidth: maxWidth) - .padding(left: 60, right: 12), + if (showExpandableComments) + _PostCommentIntent(data: data).padding(left: 60, right: 12) + else + _PostFeaturedComment(data: data, maxWidth: maxWidth) + .padding(left: 60, right: 12), Padding( padding: const EdgeInsets.only(top: 4), child: _PostReactionList( @@ -404,13 +412,24 @@ class PostShareImageWidget extends StatelessWidget { child: AutoResizeUniversalImage( sn.getAttachmentUrl(data.preload!.thumbnail!.rid), fit: BoxFit.cover, + filterQuality: FilterQuality.high, ), ), ).padding(bottom: 8), - _PostContentHeader( - data: data, - isRelativeDate: false, - ).padding(horizontal: 16, bottom: 8), + Row( + children: [ + _PostAvatar( + data: data, + isCompact: false, + filterQuality: FilterQuality.high, + ), + const Gap(12), + _PostContentHeader( + data: data, + isRelativeDate: false, + ).padding(horizontal: 16, bottom: 8), + ], + ), if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), _PostHeadline( @@ -803,7 +822,12 @@ class _PostHeadline extends StatelessWidget { class _PostAvatar extends StatelessWidget { final SnPost data; final bool isCompact; - const _PostAvatar({required this.data, required this.isCompact}); + final FilterQuality? filterQuality; + const _PostAvatar({ + required this.data, + required this.isCompact, + this.filterQuality, + }); @override Widget build(BuildContext context) { @@ -815,6 +839,7 @@ class _PostAvatar extends StatelessWidget { return GestureDetector( child: data.preload?.realm == null ? AccountImage( + filterQuality: filterQuality, content: data.publisher.avatar, radius: isCompact ? 12 : 20, borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20, @@ -827,6 +852,7 @@ class _PostAvatar extends StatelessWidget { : null, ) : AccountImage( + filterQuality: filterQuality, content: data.preload!.realm!.avatar, radius: isCompact ? 12 : 20, borderRadius: isCompact ? 4 : 8, @@ -1395,6 +1421,95 @@ class _PostTruncatedHint extends StatelessWidget { } } +class _PostCommentIntent extends StatefulWidget { + final SnPost data; + const _PostCommentIntent({required this.data}); + + @override + State<_PostCommentIntent> createState() => _PostCommentIntentState(); +} + +class _PostCommentIntentState extends State<_PostCommentIntent> { + bool _isBusy = false; + int? _totalCount; + final List _comments = List.empty(growable: true); + + bool get _isAllLoaded => + _totalCount != null && _comments.length >= _totalCount!; + + Future _fetchComments() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get( + '/cgi/co/posts/${widget.data.id}/replies', + queryParameters: { + 'take': 10, + 'offset': _comments.length, + }, + ); + _totalCount = resp.data['count']; + _comments.addAll( + resp.data['data'].map((ele) => SnPost.fromJson(ele)).cast(), + ); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (_comments.isNotEmpty) + Card( + margin: EdgeInsets.zero, + child: Column( + spacing: 8, + children: [ + for (final ele in _comments) + PostItem( + data: ele, + showExpandableComments: true, + maxWidth: double.infinity, + ).padding(vertical: 8), + ], + ), + ).padding(bottom: 8), + Row( + children: [ + Transform.flip( + flipX: true, + child: const Icon(Symbols.comment, size: 20), + ), + const Gap(4), + Text('postCommentsDetailed'.plural(widget.data.metric.replyCount)), + if (widget.data.metric.replyCount > 0 && !_isAllLoaded) + SizedBox( + width: 20, + height: 20, + child: IconButton( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + icon: const Icon(Symbols.expand_more, size: 18), + onPressed: _isBusy + ? null + : () { + _fetchComments(); + }, + ), + ).padding(left: 8), + ], + ).opacity(0.75).padding(horizontal: 4), + ], + ); + } +} + class _PostFeaturedComment extends StatefulWidget { final SnPost data; final double? maxWidth;