refactor: split the various parts of the post widget out into separate widgets

Makes the whole thing way more readable and is a preparation for reusing
code in TimelinePostScreen
This commit is contained in:
Bart Ribbers 2025-04-22 11:39:05 +02:00
parent 4f347634db
commit b0c0c22381

View file

@ -53,7 +53,6 @@ class TimelinePostWidget extends StatefulWidget {
class _TimelinePostWidgetState extends State<TimelinePostWidget> { class _TimelinePostWidgetState extends State<TimelinePostWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context);
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false; var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
return SizedBox( return SizedBox(
@ -64,30 +63,99 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( _PostHeader(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
allowDeletion: widget.allowAllDeletion ||
widget.post.creator?.userId == widget.userId,
onUserTap: widget.onUserTap,
onPostDelete: widget.onPostDelete,
),
if (widget.post.imageUrl != null || widget.post.image != null) ...[
const SizedBox(height: 8.0),
_PostImage(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
),
],
const SizedBox(height: 8.0),
_PostLikeAndReactionsInformation(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
isLikedByUser: isLikedByUser,
onTapComment: widget.onTap,
),
const SizedBox(height: 8.0),
if (widget.options.itemInfoBuilder != null) ...[
widget.options.itemInfoBuilder!(post: widget.post),
] else ...[
_PostInfo(
options: widget.options,
post: widget.post,
onTap: widget.onTap,
),
],
if (widget.options.dividerBuilder != null) ...[
widget.options.dividerBuilder!(),
],
],
),
);
}
}
class _PostHeader extends StatelessWidget {
const _PostHeader({
required this.service,
required this.options,
required this.userId,
required this.post,
required this.allowDeletion,
required this.onUserTap,
required this.onPostDelete,
});
final TimelineService service;
final TimelineOptions options;
final String userId;
final TimelinePost post;
final bool allowDeletion;
final void Function(String userId)? onUserTap;
final VoidCallback onPostDelete;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
children: [ children: [
if (widget.post.creator != null) ...[ if (post.creator != null) ...[
InkWell( InkWell(
onTap: widget.onUserTap != null onTap: onUserTap != null
? () => ? () => onUserTap?.call(post.creator!.userId)
widget.onUserTap?.call(widget.post.creator!.userId)
: null, : null,
child: Row( child: Row(
children: [ children: [
if (widget.post.creator!.imageUrl != null) ...[ if (post.creator!.imageUrl != null) ...[
widget.options.userAvatarBuilder?.call( options.userAvatarBuilder?.call(
widget.post.creator!, post.creator!,
28, 28,
) ?? ) ??
CircleAvatar( CircleAvatar(
radius: 14, radius: 14,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
widget.post.creator!.imageUrl!, post.creator!.imageUrl!,
), ),
), ),
] else ...[ ] else ...[
widget.options.anonymousAvatarBuilder?.call( options.anonymousAvatarBuilder?.call(
widget.post.creator!, post.creator!,
28, 28,
) ?? ) ??
const CircleAvatar( const CircleAvatar(
@ -99,254 +167,243 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
], ],
const SizedBox(width: 10.0), const SizedBox(width: 10.0),
Text( Text(
widget.options.nameBuilder?.call(widget.post.creator) ?? options.nameBuilder?.call(post.creator) ??
widget.post.creator?.fullName ?? post.creator?.fullName ??
widget.options.translations.anonymousUser, options.translations.anonymousUser,
style: widget.options.theme.textStyles style: options.theme.textStyles.listPostCreatorTitleStyle ??
.listPostCreatorTitleStyle ?? theme.textTheme.titleSmall!.copyWith(color: Colors.black),
theme.textTheme.titleSmall!.copyWith(
color: Colors.black,
),
), ),
], ],
), ),
), ),
], ],
const Spacer(), const Spacer(),
if (widget.allowAllDeletion || if (allowDeletion) ...[
widget.post.creator?.userId == widget.userId) ...[
PopupMenuButton( PopupMenuButton(
onSelected: (value) async { onSelected: (value) async {
if (value == 'delete') { if (value == 'delete') {
await showPostDeletionConfirmationDialog( await showPostDeletionConfirmationDialog(
widget.options, options,
context, context,
widget.onPostDelete, onPostDelete,
); );
} }
}, },
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
<PopupMenuEntry<String>>[
PopupMenuItem<String>( PopupMenuItem<String>(
value: 'delete', value: 'delete',
child: Row( child: Row(
children: [ children: [
Text( Text(
widget.options.translations.deletePost, options.translations.deletePost,
style: widget style: options.theme.textStyles.deletePostStyle ??
.options.theme.textStyles.deletePostStyle ??
theme.textTheme.bodyMedium, theme.textTheme.bodyMedium,
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
widget.options.theme.deleteIcon ?? options.theme.deleteIcon ??
Icon( Icon(
Icons.delete, Icons.delete,
color: widget.options.theme.iconColor, color: options.theme.iconColor,
), ),
], ],
), ),
), ),
], ],
child: widget.options.theme.moreIcon ?? child: options.theme.moreIcon ??
Icon( Icon(
Icons.more_horiz_rounded, Icons.more_horiz_rounded,
color: widget.options.theme.iconColor, color: options.theme.iconColor,
), ),
), ),
], ],
], ],
), );
// image of the post }
if (widget.post.imageUrl != null || widget.post.image != null) ...[ }
const SizedBox(height: 8.0),
Flexible(
flex: widget.options.postWidgetHeight != null ? 1 : 0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: widget.options.doubleTapTolike
? TappableImage(
likeAndDislikeIcon:
widget.options.likeAndDislikeIconsForDoubleTap,
post: widget.post,
userId: widget.userId,
onLike: ({required bool liked}) async {
var userId = widget.userId;
late TimelinePost result; class _PostLikeAndReactionsInformation extends StatelessWidget {
const _PostLikeAndReactionsInformation({
required this.service,
required this.options,
required this.userId,
required this.post,
required this.isLikedByUser,
required this.onTapComment,
});
if (!liked) { final TimelineService service;
result = await widget.service.postService.likePost( final TimelineOptions options;
final String userId;
final TimelinePost post;
final bool isLikedByUser;
final VoidCallback onTapComment;
@override
Widget build(BuildContext context) => Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
if (!isLikedByUser) {
await service.postService.likePost(
userId, userId,
widget.post, post,
); );
} else { } else {
result = await service.postService.unlikePost(
await widget.service.postService.unlikePost(
userId, userId,
widget.post, post,
);
}
},
icon: options.theme.likeIcon ??
Icon(
isLikedByUser
? Icons.favorite_rounded
: Icons.favorite_outline_outlined,
color: options.theme.iconColor,
size: options.iconSize,
),
),
const SizedBox(width: 4.0),
if (options.iconsWithValues) ...[
Text('${post.likes}'),
],
if (post.reactionEnabled) ...[
const SizedBox(width: 8.0),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: onTapComment,
icon: options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: options.theme.iconColor,
width: options.iconSize,
height: options.iconSize,
),
),
if (options.iconsWithValues) ...[
const SizedBox(width: 4.0),
Text('${post.reaction}'),
],
],
],
);
}
class _PostImage extends StatelessWidget {
const _PostImage({
required this.options,
required this.service,
required this.userId,
required this.post,
});
final TimelineOptions options;
final TimelineService service;
final String userId;
final TimelinePost post;
@override
Widget build(BuildContext context) => Flexible(
flex: options.postWidgetHeight != null ? 1 : 0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: options.doubleTapTolike
? TappableImage(
likeAndDislikeIcon: options.likeAndDislikeIconsForDoubleTap,
post: post,
userId: userId,
onLike: ({required bool liked}) async {
TimelinePost result;
if (!liked) {
result = await service.postService.likePost(
userId,
post,
);
} else {
result = await service.postService.unlikePost(
userId,
post,
); );
} }
return result.likedBy?.contains(userId) ?? false; return result.likedBy?.contains(userId) ?? false;
}, },
) )
: widget.post.imageUrl != null : post.imageUrl != null
? CachedNetworkImage( ? CachedNetworkImage(
width: double.infinity, width: double.infinity,
imageUrl: widget.post.imageUrl!, imageUrl: post.imageUrl!,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
) )
: Image.memory( : Image.memory(
width: double.infinity, width: double.infinity,
widget.post.image!, post.image!,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
), ),
), ),
),
],
const SizedBox(height: 8.0),
// post information
if (widget.options.iconsWithValues) ...[
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
var userId = widget.userId;
if (!isLikedByUser) {
await widget.service.postService.likePost(
userId,
widget.post,
);
} else {
await widget.service.postService.unlikePost(
userId,
widget.post,
); );
} }
},
icon: widget.options.theme.likeIcon ?? class _PostInfo extends StatelessWidget {
Icon( const _PostInfo({
isLikedByUser required this.options,
? Icons.favorite_rounded required this.post,
: Icons.favorite_outline_outlined, required this.onTap,
color: widget.options.theme.iconColor, });
size: widget.options.iconSize,
), final TimelineOptions options;
), final TimelinePost post;
const SizedBox(width: 4.0), final VoidCallback onTap;
Text('${widget.post.likes}'),
if (widget.post.reactionEnabled) ...[ @override
const SizedBox(width: 8.0), Widget build(BuildContext context) {
IconButton( var theme = Theme.of(context);
padding: EdgeInsets.zero,
constraints: const BoxConstraints(), return Column(
onPressed: widget.onTap,
icon: widget.options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: widget.options.theme.iconColor,
width: widget.options.iconSize,
height: widget.options.iconSize,
),
),
const SizedBox(width: 4.0),
Text('${widget.post.reaction}'),
],
],
),
] else ...[
Row(
children: [ children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed:
isLikedByUser ? widget.onTapUnlike : widget.onTapLike,
icon: (isLikedByUser
? widget.options.theme.likedIcon
: widget.options.theme.likeIcon) ??
Icon(
isLikedByUser
? Icons.favorite_rounded
: Icons.favorite_outline,
color: widget.options.theme.iconColor,
size: widget.options.iconSize,
),
),
const SizedBox(width: 8.0),
if (widget.post.reactionEnabled) ...[
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: widget.onTap,
icon: widget.options.theme.commentIcon ??
SvgPicture.asset(
'assets/Comment.svg',
package: 'flutter_timeline_view',
// ignore: deprecated_member_use
color: widget.options.theme.iconColor,
width: widget.options.iconSize,
height: widget.options.iconSize,
),
),
],
],
),
],
const SizedBox(height: 8),
if (widget.options.itemInfoBuilder != null) ...[
widget.options.itemInfoBuilder!(
post: widget.post,
),
] else ...[
_PostLikeCountText( _PostLikeCountText(
post: widget.post, post: post,
options: widget.options, options: options,
), ),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
Row( Row(
children: [ children: [
Text( Text(
widget.options.nameBuilder?.call(widget.post.creator) ?? options.nameBuilder?.call(post.creator) ??
widget.post.creator?.fullName ?? post.creator?.fullName ??
widget.options.translations.anonymousUser, options.translations.anonymousUser,
style: widget.options.theme.textStyles.listCreatorNameStyle ?? style: options.theme.textStyles.listCreatorNameStyle ??
theme.textTheme.titleSmall!.copyWith( theme.textTheme.titleSmall!.copyWith(
color: Colors.black, color: Colors.black,
), ),
), ),
const SizedBox(width: 4.0), const SizedBox(width: 4.0),
Text( Text(
widget.post.title, post.title,
style: widget.options.theme.textStyles.listPostTitleStyle ?? style: options.theme.textStyles.listPostTitleStyle ??
theme.textTheme.bodySmall, theme.textTheme.bodySmall,
), ),
], ],
), ),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
InkWell( InkWell(
onTap: widget.onTap, onTap: onTap,
child: Text( child: Text(
widget.options.translations.viewPost, options.translations.viewPost,
style: widget.options.theme.textStyles.viewPostStyle ?? style: options.theme.textStyles.viewPostStyle ??
theme.textTheme.titleSmall!.copyWith( theme.textTheme.titleSmall!.copyWith(
color: const Color(0xFF8D8D8D), color: const Color(0xFF8D8D8D),
), ),
), ),
), ),
], ],
if (widget.options.dividerBuilder != null) ...[
widget.options.dividerBuilder!(),
],
],
),
); );
} }
} }