💄 Optimize post comments

This commit is contained in:
LittleSheep 2025-03-19 00:21:54 +08:00
parent d6013078bd
commit e68ada2d04
2 changed files with 308 additions and 250 deletions

View File

@ -103,7 +103,7 @@ class OpenablePostItem extends StatelessWidget {
transitionType: ContainerTransitionType.fade, transitionType: ContainerTransitionType.fade,
closedElevation: 0, closedElevation: 0,
closedColor: Theme.of(context).colorScheme.surface.withOpacity( closedColor: Theme.of(context).colorScheme.surface.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1,
), ),
closedShape: const RoundedRectangleBorder( closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
@ -122,6 +122,7 @@ class PostItem extends StatefulWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showAvatar; final bool showAvatar;
final bool showCompactAvatar;
final bool showExpandableComments; final bool showExpandableComments;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
@ -137,6 +138,7 @@ class PostItem extends StatefulWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showAvatar = true, this.showAvatar = true,
this.showCompactAvatar = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
@ -297,185 +299,180 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Container( Container(
constraints: constraints: BoxConstraints(
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity), maxWidth: widget.maxWidth ?? double.infinity,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Row(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, if (widget.showAvatar)
children: [ _PostAvatar(
Row( data: widget.data,
isCompact: false,
),
if (widget.showAvatar) const Gap(12),
Expanded(
child: Row(
children: [ children: [
if (widget.showAvatar) if (widget.showCompactAvatar)
_PostAvatar( _PostAvatar(
data: widget.data, data: widget.data,
isCompact: false, isCompact: true,
), ),
if (widget.showAvatar) const Gap(12), if (widget.showAvatar) const Gap(8),
Expanded( _PostContentHeader(
child: _PostContentHeader( isRelativeDate: !widget.showFullPost,
isRelativeDate: !widget.showFullPost, isCompact: false,
isCompact: false,
data: widget.data,
),
),
_PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer,
onDeleted: () {
widget.onDeleted?.call();
},
onTranslate: () {
_translateText();
},
), ),
], ],
), ),
const Gap(8), ),
if (widget.data.preload?.thumbnail != null) _PostActionPopup(
Container( data: widget.data,
margin: const EdgeInsets.only(bottom: 8), isAuthor: isAuthor,
decoration: BoxDecoration( onShare: () => _doShare(context),
borderRadius: const BorderRadius.all( onShareImage: () => _doShareViaPicture(context),
Radius.circular(8), onSelectAnswer: widget.onSelectAnswer,
), onDeleted: () {
border: Border.all( widget.onDeleted?.call();
color: Theme.of(context).dividerColor, },
width: 1, onTranslate: () {
), _translateText();
), },
child: AspectRatio( ),
aspectRatio: 16 / 9, ],
child: ClipRRect( ),
borderRadius: const BorderRadius.all( const Gap(8),
Radius.circular(8), if (widget.data.preload?.thumbnail != null)
), Container(
child: AutoResizeUniversalImage( margin: const EdgeInsets.only(bottom: 8),
sn.getAttachmentUrl( decoration: BoxDecoration(
widget.data.preload!.thumbnail!.rid, borderRadius: const BorderRadius.all(
), Radius.circular(8),
fit: BoxFit.cover,
),
),
),
),
if (widget.data.preload?.video != null)
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
if (widget.data.type == 'question')
_PostQuestionHint(data: widget.data).padding(bottom: 8),
if (_displayDescription.isNotEmpty ||
_displayTitle.isNotEmpty)
_PostHeadline(
title: _displayTitle,
description: _displayDescription,
data: widget.data,
isEnlarge: widget.data.type == 'article' &&
widget.showFullPost,
).padding(bottom: 8),
if (widget.data.type == 'article' && !widget.showFullPost)
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(bottom: 8),
if ((_displayText.isNotEmpty) &&
(widget.showFullPost ||
widget.data.type != 'article'))
_PostContentBody(
text: _displayText,
data: widget.data,
isSelectable: widget.showFullPost,
isEnlarge: widget.data.type == 'article' &&
widget.showFullPost,
).padding(bottom: 6),
if (widget.data.visibility > 0)
_PostVisibilityHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.body['content_truncated'] == true)
_PostTruncatedHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.tags.isNotEmpty)
_PostTagsList(data: widget.data)
.padding(top: 4, bottom: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (widget.showViews)
Row(
children: [
Icon(Symbols.play_circle, size: 20),
const Gap(4),
Text('postViews')
.plural(widget.data.totalViews),
],
).opacity(0.75),
if (_isTranslating)
AnimateWidgetExtensions(Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translating').tr(),
],
))
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
if (_isTranslated)
InkWell(
child: Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translated').tr(),
],
).opacity(0.75),
onTap: () {
setState(() {
_displayText =
widget.data.body['content'] ?? '';
_displayTitle =
widget.data.body['title'] ?? '';
_displayDescription =
widget.data.body['description'] ?? '';
_isTranslated = false;
});
},
),
if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!)
.padding(
top: 4,
bottom: widget.data.preload?.attachments
?.isNotEmpty ??
false
? 12
: 0,
),
],
).padding(
bottom:
widget.showViews || _isTranslated || _isTranslating
? 8
: 0,
), ),
], border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid,
),
fit: BoxFit.cover,
),
),
),
), ),
).padding(horizontal: 12, top: 8), if (widget.data.preload?.video != null)
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
if (widget.data.type == 'question')
_PostQuestionHint(data: widget.data).padding(bottom: 8),
if (_displayDescription.isNotEmpty || _displayTitle.isNotEmpty)
_PostHeadline(
title: _displayTitle,
description: _displayDescription,
data: widget.data,
isEnlarge:
widget.data.type == 'article' && widget.showFullPost,
).padding(bottom: 8),
if (widget.data.type == 'article' && !widget.showFullPost)
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(bottom: 8),
if ((_displayText.isNotEmpty) &&
(widget.showFullPost || widget.data.type != 'article'))
_PostContentBody(
text: _displayText,
data: widget.data,
isSelectable: widget.showFullPost,
isEnlarge:
widget.data.type == 'article' && widget.showFullPost,
).padding(bottom: 6),
if (widget.data.visibility > 0)
_PostVisibilityHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.body['content_truncated'] == true)
_PostTruncatedHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.tags.isNotEmpty)
_PostTagsList(data: widget.data).padding(top: 4, bottom: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (widget.showViews)
Row(
children: [
Icon(Symbols.play_circle, size: 20),
const Gap(4),
Text('postViews').plural(widget.data.totalViews),
],
).opacity(0.75),
if (_isTranslating)
AnimateWidgetExtensions(Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translating').tr(),
],
))
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
if (_isTranslated)
InkWell(
child: Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translated').tr(),
],
).opacity(0.75),
onTap: () {
setState(() {
_displayText = widget.data.body['content'] ?? '';
_displayTitle = widget.data.body['title'] ?? '';
_displayDescription =
widget.data.body['description'] ?? '';
_isTranslated = false;
});
},
),
if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!).padding(
top: 4,
bottom: widget.data.preload?.attachments?.isNotEmpty ??
false
? 12
: 0,
),
],
).padding(
bottom: widget.showViews || _isTranslated || _isTranslating
? 8
: 0,
),
], ],
), ).padding(horizontal: 12, top: 8),
), ),
if (displayableAttachments?.isNotEmpty ?? false) if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
@ -509,6 +506,7 @@ class _PostItemState extends State<PostItem> {
_PostCommentIntent( _PostCommentIntent(
data: widget.data, data: widget.data,
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
maxWidth: widget.maxWidth ?? double.infinity,
).padding(left: 12, right: 12) ).padding(left: 12, right: 12)
else else
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth) _PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
@ -558,10 +556,22 @@ class _PostItemState extends State<PostItem> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _PostContentHeader( child: Row(
isRelativeDate: !widget.showFullPost, children: [
isCompact: true, if (widget.showCompactAvatar)
data: widget.data, _PostAvatar(
data: widget.data,
isCompact: true,
),
if (widget.showCompactAvatar) const Gap(8),
Expanded(
child: _PostContentHeader(
isRelativeDate: !widget.showFullPost,
isCompact: true,
data: widget.data,
),
),
],
), ),
), ),
_PostActionPopup( _PostActionPopup(
@ -578,7 +588,7 @@ class _PostItemState extends State<PostItem> {
}, },
), ),
], ],
), ).padding(bottom: widget.showCompactAvatar ? 4 : 0),
if (widget.data.preload?.thumbnail != null) if (widget.data.preload?.thumbnail != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@ -755,19 +765,28 @@ class _PostItemState extends State<PostItem> {
if (widget.showExpandableComments) if (widget.showExpandableComments)
_PostCommentIntent( _PostCommentIntent(
data: widget.data, data: widget.data,
maxWidth: (widget.maxWidth ?? double.infinity) -
(widget.showAvatar ? 72 : 24),
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
).padding(left: widget.showAvatar ? 60 : 12, right: 12) ).padding(left: widget.showAvatar ? 60 : 12, right: 12)
else if (widget.showComments) else if (widget.showComments)
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth) _PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
.padding(left: widget.showAvatar ? 60 : 12, right: 12), .padding(left: widget.showAvatar ? 60 : 12, right: 12),
if (widget.showReactions) if (widget.showReactions)
Padding( Container(
padding: const EdgeInsets.only(top: 4), constraints: BoxConstraints(
child: _PostReactionList( maxWidth: widget.maxWidth ?? double.infinity,
data: widget.data, ),
padding: child: Padding(
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12), padding: const EdgeInsets.only(top: 4),
onChanged: _onChanged, child: _PostReactionList(
data: widget.data,
padding: EdgeInsets.only(
left: widget.showAvatar ? 60 : 12,
right: 12,
),
onChanged: _onChanged,
),
), ),
), ),
], ],
@ -1552,19 +1571,24 @@ class _PostContentHeader extends StatelessWidget {
if (isCompact) { if (isCompact) {
return Row( return Row(
children: [ children: [
Text(data.publisher.nick).bold(), Flexible(
child: Text(
data.publisher.nick,
maxLines: 1,
).bold(),
),
const Gap(4), const Gap(4),
Row( Flexible(
children: [ child: Text(
Text( isRelativeDate
isRelativeDate ? RelativeTime(context)
? RelativeTime(context) .format((data.publishedAt ?? data.createdAt).toLocal())
.format((data.publishedAt ?? data.createdAt).toLocal()) : DateFormat('y/M/d HH:mm')
: DateFormat('y/M/d HH:mm') .format((data.publishedAt ?? data.createdAt).toLocal()),
.format((data.publishedAt ?? data.createdAt).toLocal()), maxLines: 1,
).fontSize(13), overflow: TextOverflow.fade,
], ).fontSize(13).opacity(0.8),
).opacity(0.8), ),
], ],
); );
} else { } else {
@ -1573,25 +1597,39 @@ class _PostContentHeader extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
Text(data.publisher.nick).bold(), Flexible(
child: Text(data.publisher.nick).bold(),
),
if (data.preload?.realm != null) if (data.preload?.realm != null)
const Icon(Symbols.arrow_right, size: 16) const Icon(Symbols.arrow_right, size: 16)
.padding(horizontal: 2) .padding(horizontal: 2)
.opacity(0.5), .opacity(0.5),
if (data.preload?.realm != null) Text(data.preload!.realm!.name), if (data.preload?.realm != null)
Flexible(
child: Text(data.preload!.realm!.name),
),
], ],
), ),
Row( Row(
children: [ children: [
Text('@${data.publisher.name}').fontSize(13), Flexible(
child: Text(
'@${data.publisher.name}',
maxLines: 1,
).fontSize(13),
),
const Gap(4), const Gap(4),
Text( Flexible(
isRelativeDate child: Text(
? RelativeTime(context) isRelativeDate
.format((data.publishedAt ?? data.createdAt).toLocal()) ? RelativeTime(context).format(
: DateFormat('y/M/d HH:mm') (data.publishedAt ?? data.createdAt).toLocal())
.format((data.publishedAt ?? data.createdAt).toLocal()), : DateFormat('y/M/d HH:mm').format(
).fontSize(13), (data.publishedAt ?? data.createdAt).toLocal()),
maxLines: 1,
overflow: TextOverflow.fade,
).fontSize(13),
),
], ],
).opacity(0.8), ).opacity(0.8),
], ],
@ -1856,7 +1894,12 @@ class _PostTruncatedHint extends StatelessWidget {
class _PostCommentIntent extends StatefulWidget { class _PostCommentIntent extends StatefulWidget {
final SnPost data; final SnPost data;
final bool showAvatar; final bool showAvatar;
const _PostCommentIntent({required this.data, this.showAvatar = false}); final double maxWidth;
const _PostCommentIntent({
required this.data,
this.showAvatar = false,
required this.maxWidth,
});
@override @override
State<_PostCommentIntent> createState() => _PostCommentIntentState(); State<_PostCommentIntent> createState() => _PostCommentIntentState();
@ -1895,54 +1938,69 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Container(
children: [ constraints: BoxConstraints(maxWidth: widget.maxWidth),
if (_comments.isNotEmpty) child: Column(
Card( children: [
elevation: 4, if (_comments.isNotEmpty)
margin: EdgeInsets.zero, Card(
child: Column( elevation: 4,
spacing: 8, margin: EdgeInsets.zero,
children: [ child: Column(
for (final ele in _comments) spacing: 8,
PostItem( children: [
data: ele, for (final ele in _comments)
showAvatar: false, InkWell(
showExpandableComments: true, borderRadius: const BorderRadius.all(Radius.circular(8)),
showReactions: false, child: PostItem(
showViews: false, data: ele,
maxWidth: double.infinity, showAvatar: false,
).padding(vertical: 8, left: 6), showCompactAvatar: true,
], showExpandableComments: true,
), showReactions: false,
).padding(vertical: 8), showViews: false,
Row( maxWidth: double.infinity,
children: [ ).padding(vertical: 8, left: 6),
Transform.flip( onTap: () {
flipX: true, GoRouter.of(context).pushNamed(
child: const Icon(Symbols.comment, size: 20), 'postDetail',
), pathParameters: {'slug': ele.id.toString()},
const Gap(4), extra: ele,
Text('postCommentsDetailed'.plural(widget.data.metric.replyCount)), );
if (widget.data.metric.replyCount > 0 && !_isAllLoaded) },
SizedBox( ),
width: 20, ],
height: 20, ),
child: IconButton( ).padding(vertical: 8),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), Row(
constraints: const BoxConstraints(), children: [
padding: EdgeInsets.zero, Transform.flip(
icon: const Icon(Symbols.expand_more, size: 18), flipX: true,
onPressed: _isBusy child: const Icon(Symbols.comment, size: 20),
? null ),
: () { const Gap(4),
_fetchComments(); Text(
}, 'postCommentsDetailed'.plural(widget.data.metric.replyCount)),
), if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
).padding(left: 8), SizedBox(
], width: 20,
).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0), 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: widget.showAvatar ? 4 : 0),
],
),
); );
} }
} }

View File

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 2.4.2+81 version: 2.4.2+82
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4