💄 Optimize post reply preview

This commit is contained in:
2025-12-06 13:53:22 +08:00
parent 504e4d55ad
commit 60b8e2bcad

View File

@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -66,6 +68,7 @@ class PostReplyPreview extends HookConsumerWidget {
final bool isOpenable; final bool isOpenable;
final bool isCompact; final bool isCompact;
final bool isAutoload; final bool isAutoload;
final double? itemMaxWidth;
final VoidCallback? onOpen; final VoidCallback? onOpen;
const PostReplyPreview({ const PostReplyPreview({
super.key, super.key,
@@ -73,6 +76,7 @@ class PostReplyPreview extends HookConsumerWidget {
this.isOpenable = false, this.isOpenable = false,
this.isCompact = false, this.isCompact = false,
this.isAutoload = true, this.isAutoload = true,
this.itemMaxWidth,
this.onOpen, this.onOpen,
}); });
@@ -114,39 +118,49 @@ class PostReplyPreview extends HookConsumerWidget {
return null; return null;
}, [parent]); }, [parent]);
final featuredReply = final featuredReply = isOpenable
isOpenable ? null : ref.watch(postFeaturedReplyProvider(parent.id)); ? null
: ref.watch(postFeaturedReplyProvider(parent.id));
final itemWidget = Widget itemBuilder(double maxWidth) {
isOpenable return isOpenable
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
for (final post in posts.value) for (final post in posts.value)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InkWell( InkWell(
child: Row( child: ConstrainedBox(
crossAxisAlignment: CrossAxisAlignment.start, constraints: BoxConstraints(maxWidth: maxWidth),
spacing: 8, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
ProfilePictureWidget( spacing: 8,
file: post.publisher.picture, children: [
radius: 12, ProfilePictureWidget(
).padding(top: 4), file: post.publisher.picture,
if (post.content?.isNotEmpty ?? false) radius: 12,
Expanded( ).padding(top: 4),
child: MarkdownTextContent( if (post.content?.isNotEmpty ?? false)
content: post.content!, Expanded(
attachments: post.attachments, child: MarkdownTextContent(
).padding(top: 2), content: post.content!,
) attachments: post.attachments,
else ).padding(top: 2),
Expanded( )
child: Text( else
'postHasAttachments', Expanded(
).plural(post.attachments.length), child:
), Text(
], 'postHasAttachments',
style: TextStyle(height: 2),
)
.plural(post.attachments.length)
.padding(top: 2),
),
],
),
), ),
onTap: () { onTap: () {
onOpen?.call(); onOpen?.call();
@@ -162,12 +176,14 @@ class PostReplyPreview extends HookConsumerWidget {
isOpenable: true, isOpenable: true,
isCompact: true, isCompact: true,
isAutoload: false, isAutoload: false,
itemMaxWidth: math.max(maxWidth - 24, 200),
onOpen: onOpen, onOpen: onOpen,
).padding(left: 24), ).padding(left: 24),
], ],
), ),
if (loading.value) if (loading.value)
Row( Row(
mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
SizedBox( SizedBox(
@@ -179,8 +195,9 @@ class PostReplyPreview extends HookConsumerWidget {
], ],
) )
else if (posts.value.length < parent.repliesCount) else if (posts.value.length < parent.repliesCount)
InkWell( GestureDetector(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
const Icon(Symbols.keyboard_arrow_down, size: 20), const Icon(Symbols.keyboard_arrow_down, size: 20),
@@ -193,81 +210,87 @@ class PostReplyPreview extends HookConsumerWidget {
), ),
], ],
) )
: (featuredReply!).map( : (featuredReply!).map(
data: data: (data) => ConstrainedBox(
(data) => Row( constraints: BoxConstraints(maxWidth: maxWidth),
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
spacing: 8, crossAxisAlignment: CrossAxisAlignment.start,
children: [ spacing: 8,
ProfilePictureWidget( children: [
file: data.value?.publisher.picture, ProfilePictureWidget(
radius: 12, file: data.value?.publisher.picture,
).padding(top: 4), radius: 12,
if (data.value?.content?.isNotEmpty ?? false) ).padding(top: 4),
Expanded( if (data.value?.content?.isNotEmpty ?? false)
child: MarkdownTextContent( Expanded(
content: data.value!.content!, child: MarkdownTextContent(
attachments: data.value!.attachments, content: data.value!.content!,
), attachments: data.value!.attachments,
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(data.value?.attachments.length ?? 0),
), ),
], )
), else
error: Expanded(
(e) => Row( child: Text(
spacing: 8, 'postHasAttachments',
children: [ ).plural(data.value?.attachments.length ?? 0),
const Icon(Symbols.close, size: 18),
Text(e.error.toString()),
],
),
loading:
(_) => Row(
spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
), ),
Text('loading').tr(), ],
],
),
);
final contentWidget =
isCompact
? itemWidget
: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
), ),
borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: Column( error: (e) => Row(
crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 8,
spacing: 4,
children: [ children: [
Text('repliesCount') const Icon(Symbols.close, size: 18),
.plural(parent.repliesCount) Text(e.error.toString()),
.fontSize(15) ],
.bold() ),
.padding(horizontal: 5), loading: (_) => Row(
itemWidget, spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
),
Text('loading').tr(),
], ],
), ),
); );
}
return InkWell( final contentWidget = isCompact
borderRadius: const BorderRadius.all(Radius.circular(8)), ? itemBuilder(itemMaxWidth ?? MediaQuery.of(context).size.width)
: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: LayoutBuilder(
builder: (context, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.fontSize(15)
.bold()
.padding(horizontal: 5),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: itemBuilder(constraints.maxWidth),
),
],
);
},
),
);
return GestureDetector(
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -479,8 +502,9 @@ class ReferencedPostWidget extends StatelessWidget {
referencePost.description!, referencePost.description!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: color: Theme.of(
Theme.of(context).colorScheme.onSurfaceVariant, context,
).colorScheme.onSurfaceVariant,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -490,10 +514,9 @@ class ReferencedPostWidget extends StatelessWidget {
content: referencePost.content!, content: referencePost.content!,
textStyle: const TextStyle(fontSize: 14), textStyle: const TextStyle(fontSize: 14),
isSelectable: false, isSelectable: false,
linesMargin: linesMargin: referencePost.type == 0
referencePost.type == 0 ? const EdgeInsets.only(bottom: 4)
? const EdgeInsets.only(bottom: 4) : null,
: null,
attachments: item.attachments, attachments: item.attachments,
).padding(bottom: 4), ).padding(bottom: 4),
if (referencePost.isTruncated) if (referencePost.isTruncated)
@@ -537,11 +560,10 @@ class ReferencedPostWidget extends StatelessWidget {
} }
return content.gestures( return content.gestures(
onTap: onTap: () => context.pushNamed(
() => context.pushNamed( 'postDetail',
'postDetail', pathParameters: {'id': referencePost!.id},
pathParameters: {'id': referencePost!.id}, ),
),
); );
} }
} }
@@ -577,15 +599,14 @@ class PostHeader extends StatelessWidget {
spacing: 12, spacing: 12,
children: [ children: [
GestureDetector( GestureDetector(
onTap: onTap: isInteractive
isInteractive ? () {
? () { context.pushNamed(
context.pushNamed( 'publisherProfile',
'publisherProfile', pathParameters: {'name': item.publisher.name},
pathParameters: {'name': item.publisher.name}, );
); }
} : null,
: null,
child: ProfilePictureWidget( child: ProfilePictureWidget(
file: file:
item.publisher.picture ?? item.publisher.picture ??
@@ -606,19 +627,19 @@ class PostHeader extends StatelessWidget {
Flexible( Flexible(
child: child:
(item.publisher.account != null && (item.publisher.account != null &&
item.publisher.type == 0) item.publisher.type == 0)
? AccountName( ? AccountName(
hideOverlay: hideOverlay, hideOverlay: hideOverlay,
account: item.publisher.account!, account: item.publisher.account!,
textOverride: item.publisher.nick, textOverride: item.publisher.nick,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
hideVerificationMark: true, hideVerificationMark: true,
) )
: Text( : Text(
item.publisher.nick, item.publisher.nick,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).bold(), ).bold(),
), ),
if (item.publisher.verification != null) if (item.publisher.verification != null)
VerificationMark( VerificationMark(
@@ -627,14 +648,13 @@ class PostHeader extends StatelessWidget {
), ),
if (item.realm == null) if (item.realm == null)
Flexible( Flexible(
child: child: isCompact
isCompact ? const SizedBox.shrink()
? const SizedBox.shrink() : Text(
: Text( '@${item.publisher.name}',
'@${item.publisher.name}', maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ).fontSize(11),
).fontSize(11),
) )
else else
...([ ...([
@@ -673,8 +693,8 @@ class PostHeader extends StatelessWidget {
Text( Text(
!isFullPost && isRelativeTime !isFullPost && isRelativeTime
? (item.publishedAt ?? item.createdAt)!.formatRelative( ? (item.publishedAt ?? item.createdAt)!.formatRelative(
context, context,
) )
: (item.publishedAt ?? item.createdAt)!.formatSystem(), : (item.publishedAt ?? item.createdAt)!.formatSystem(),
).fontSize(10), ).fontSize(10),
], ],
@@ -734,15 +754,14 @@ class PostBody extends ConsumerWidget {
const Icon(Symbols.label, size: 16).padding(top: 2), const Icon(Symbols.label, size: 16).padding(top: 2),
for (final tag in isFullPost ? item.tags : item.tags.take(3)) for (final tag in isFullPost ? item.tags : item.tags.take(3))
InkWell( InkWell(
onTap: onTap: isInteractive
isInteractive ? () {
? () { GoRouter.of(context).pushNamed(
GoRouter.of(context).pushNamed( 'postTagDetail',
'postTagDetail', pathParameters: {'slug': tag.slug},
pathParameters: {'slug': tag.slug}, );
); }
} : null,
: null,
child: Text('#${tag.name ?? tag.slug}'), child: Text('#${tag.name ?? tag.slug}'),
), ),
if (!isFullPost && item.tags.length > 3) if (!isFullPost && item.tags.length > 3)
@@ -761,15 +780,14 @@ class PostBody extends ConsumerWidget {
for (final category for (final category
in isFullPost ? item.categories : item.categories.take(2)) in isFullPost ? item.categories : item.categories.take(2))
InkWell( InkWell(
onTap: onTap: isInteractive
isInteractive ? () {
? () { GoRouter.of(context).pushNamed(
GoRouter.of(context).pushNamed( 'postCategoryDetail',
'postCategoryDetail', pathParameters: {'slug': category.slug},
pathParameters: {'slug': category.slug}, );
); }
} : null,
: null,
child: Text(category.categoryDisplayTitle), child: Text(category.categoryDisplayTitle),
), ),
if (!isFullPost && item.categories.length > 2) if (!isFullPost && item.categories.length > 2)
@@ -798,12 +816,11 @@ class PostBody extends ConsumerWidget {
hideOverlay hideOverlay
? text ? text
: Tooltip( : Tooltip(
message: message: !isFullPost && isRelativeTime
!isFullPost && isRelativeTime ? item.editedAt!.formatSystem()
? item.editedAt!.formatSystem() : item.editedAt!.formatRelative(context),
: item.editedAt!.formatRelative(context), child: text,
child: text, ),
),
], ],
), ),
); );
@@ -936,10 +953,9 @@ class PostBody extends ConsumerWidget {
], ],
).padding(bottom: 4), ).padding(bottom: 4),
MarkdownTextContent( MarkdownTextContent(
content: content: item.isTruncated
item.isTruncated ? '${item.content!}...'
? '${item.content!}...' : item.content ?? '',
: item.content ?? '',
isSelectable: isTextSelectable, isSelectable: isTextSelectable,
attachments: item.attachments, attachments: item.attachments,
), ),