From 5abc859c24ab19396daebcf413405a83bbcc17a4 Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Tue, 22 Apr 2025 11:14:20 +0200 Subject: [PATCH 1/4] fix: add some spacing between the post author and it's title --- CHANGELOG.md | 4 ++ .../lib/src/widgets/timeline_post_widget.dart | 69 +++++++++---------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc17f87..f700bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Next + +- Add minimal spacing between a post author and title in the post widget + ## 5.1.1 - Be honest about which Dart and Flutter versions we support diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 103aac9..1ff7447 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -97,7 +97,7 @@ class _TimelinePostWidgetState extends State { ), ), ], - const SizedBox(width: 10), + const SizedBox(width: 10.0), Text( widget.options.nameBuilder?.call(widget.post.creator) ?? widget.post.creator?.fullName ?? @@ -137,7 +137,7 @@ class _TimelinePostWidgetState extends State { .options.theme.textStyles.deletePostStyle ?? theme.textTheme.bodyMedium, ), - const SizedBox(width: 8), + const SizedBox(width: 8.0), widget.options.theme.deleteIcon ?? Icon( Icons.delete, @@ -158,7 +158,7 @@ class _TimelinePostWidgetState extends State { ), // image of the post if (widget.post.imageUrl != null || widget.post.image != null) ...[ - const SizedBox(height: 8), + const SizedBox(height: 8.0), Flexible( flex: widget.options.postWidgetHeight != null ? 1 : 0, child: ClipRRect( @@ -204,9 +204,7 @@ class _TimelinePostWidgetState extends State { ), ), ], - const SizedBox( - height: 8, - ), + const SizedBox(height: 8.0), // post information if (widget.options.iconsWithValues) ...[ Row( @@ -238,14 +236,10 @@ class _TimelinePostWidgetState extends State { size: widget.options.iconSize, ), ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4.0), Text('${widget.post.likes}'), if (widget.post.reactionEnabled) ...[ - const SizedBox( - width: 8, - ), + const SizedBox(width: 8.0), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -260,9 +254,7 @@ class _TimelinePostWidgetState extends State { height: widget.options.iconSize, ), ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4.0), Text('${widget.post.reaction}'), ], ], @@ -286,7 +278,7 @@ class _TimelinePostWidgetState extends State { size: widget.options.iconSize, ), ), - const SizedBox(width: 8), + const SizedBox(width: 8.0), if (widget.post.reactionEnabled) ...[ IconButton( padding: EdgeInsets.zero, @@ -307,9 +299,7 @@ class _TimelinePostWidgetState extends State { ), ], - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (widget.options.itemInfoBuilder != null) ...[ widget.options.itemInfoBuilder!( @@ -320,25 +310,27 @@ class _TimelinePostWidgetState extends State { post: widget.post, options: widget.options, ), - Text.rich( - TextSpan( - text: widget.options.nameBuilder?.call(widget.post.creator) ?? - widget.post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles.listCreatorNameStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - children: [ - TextSpan( - text: widget.post.title, - style: widget.options.theme.textStyles.listPostTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], - ), + const SizedBox(height: 4.0), + Row( + children: [ + Text( + widget.options.nameBuilder?.call(widget.post.creator) ?? + widget.post.creator?.fullName ?? + widget.options.translations.anonymousUser, + style: widget.options.theme.textStyles.listCreatorNameStyle ?? + theme.textTheme.titleSmall!.copyWith( + color: Colors.black, + ), + ), + const SizedBox(width: 4.0), + Text( + widget.post.title, + style: widget.options.theme.textStyles.listPostTitleStyle ?? + theme.textTheme.bodySmall, + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 4.0), InkWell( onTap: widget.onTap, child: Text( @@ -350,8 +342,9 @@ class _TimelinePostWidgetState extends State { ), ), ], - if (widget.options.dividerBuilder != null) + if (widget.options.dividerBuilder != null) ...[ widget.options.dividerBuilder!(), + ], ], ), ); From 4f347634db14243fac2d0e8198486af3ecffc65e Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Tue, 22 Apr 2025 11:16:40 +0200 Subject: [PATCH 2/4] fix: use listPostCreatorTitleStyle for post creator rather than postCreatorTitleStyle It's shown in a list and the rest of the text in the widget is already styled through the list* parameters, so it only makes sense for this one to be too. --- CHANGELOG.md | 1 + .../lib/src/screens/timeline_post_screen.dart | 2 +- .../lib/src/widgets/timeline_post_widget.dart | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f700bf6..f3cb830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Next - Add minimal spacing between a post author and title in the post widget +- Use listPostCreatorTitleStyle for post creator localizations when showing posts in a list ## 5.1.1 diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index 450f207..aca8e76 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -213,7 +213,7 @@ class _TimelinePostScreenState extends State { post.creator?.fullName ?? widget.options.translations.anonymousUser, style: widget.options.theme.textStyles - .postCreatorTitleStyle ?? + .listPostCreatorTitleStyle ?? theme.textTheme.titleSmall!.copyWith( color: Colors.black, ), diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index 1ff7447..cef71c5 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -103,7 +103,7 @@ class _TimelinePostWidgetState extends State { widget.post.creator?.fullName ?? widget.options.translations.anonymousUser, style: widget.options.theme.textStyles - .postCreatorTitleStyle ?? + .listPostCreatorTitleStyle ?? theme.textTheme.titleSmall!.copyWith( color: Colors.black, ), From b0c0c22381041ea3a0769f0e87d3c57ed0462408 Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Tue, 22 Apr 2025 11:39:05 +0200 Subject: [PATCH 3/4] 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 --- .../lib/src/widgets/timeline_post_widget.dart | 591 ++++++++++-------- 1 file changed, 324 insertions(+), 267 deletions(-) diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index cef71c5..e6b35f3 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -53,7 +53,6 @@ class TimelinePostWidget extends StatefulWidget { class _TimelinePostWidgetState extends State { @override Widget build(BuildContext context) { - var theme = Theme.of(context); var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false; return SizedBox( @@ -64,282 +63,42 @@ class _TimelinePostWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - if (widget.post.creator != null) ...[ - InkWell( - onTap: widget.onUserTap != null - ? () => - widget.onUserTap?.call(widget.post.creator!.userId) - : null, - child: Row( - children: [ - if (widget.post.creator!.imageUrl != null) ...[ - widget.options.userAvatarBuilder?.call( - widget.post.creator!, - 28, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: CachedNetworkImageProvider( - widget.post.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - widget.post.creator!, - 28, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10.0), - Text( - widget.options.nameBuilder?.call(widget.post.creator) ?? - widget.post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles - .listPostCreatorTitleStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - ], - ), - ), - ], - const Spacer(), - if (widget.allowAllDeletion || - widget.post.creator?.userId == widget.userId) ...[ - PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await showPostDeletionConfirmationDialog( - widget.options, - context, - widget.onPostDelete, - ); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Text( - widget.options.translations.deletePost, - style: widget - .options.theme.textStyles.deletePostStyle ?? - theme.textTheme.bodyMedium, - ), - const SizedBox(width: 8.0), - widget.options.theme.deleteIcon ?? - Icon( - Icons.delete, - color: widget.options.theme.iconColor, - ), - ], - ), - ), - ], - child: widget.options.theme.moreIcon ?? - Icon( - Icons.more_horiz_rounded, - color: widget.options.theme.iconColor, - ), - ), - ], - ], + _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, ), - // 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; - - if (!liked) { - result = await widget.service.postService.likePost( - userId, - widget.post, - ); - } else { - result = - await widget.service.postService.unlikePost( - userId, - widget.post, - ); - } - - return result.likedBy?.contains(userId) ?? false; - }, - ) - : widget.post.imageUrl != null - ? CachedNetworkImage( - width: double.infinity, - imageUrl: widget.post.imageUrl!, - fit: BoxFit.fitWidth, - ) - : Image.memory( - width: double.infinity, - widget.post.image!, - fit: BoxFit.fitWidth, - ), - ), + _PostImage( + service: widget.service, + options: widget.options, + userId: widget.userId, + post: widget.post, ), ], 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 ?? - Icon( - isLikedByUser - ? Icons.favorite_rounded - : Icons.favorite_outline_outlined, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ), - ), - const SizedBox(width: 4.0), - Text('${widget.post.likes}'), - if (widget.post.reactionEnabled) ...[ - const SizedBox(width: 8.0), - 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(width: 4.0), - Text('${widget.post.reaction}'), - ], - ], - ), - ] else ...[ - Row( - 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), - + _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, - ), + widget.options.itemInfoBuilder!(post: widget.post), ] else ...[ - _PostLikeCountText( - post: widget.post, + _PostInfo( options: widget.options, - ), - const SizedBox(height: 4.0), - Row( - children: [ - Text( - widget.options.nameBuilder?.call(widget.post.creator) ?? - widget.post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles.listCreatorNameStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - const SizedBox(width: 4.0), - Text( - widget.post.title, - style: widget.options.theme.textStyles.listPostTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], - ), - const SizedBox(height: 4.0), - InkWell( + post: widget.post, onTap: widget.onTap, - child: Text( - widget.options.translations.viewPost, - style: widget.options.theme.textStyles.viewPostStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: const Color(0xFF8D8D8D), - ), - ), ), ], if (widget.options.dividerBuilder != null) ...[ @@ -351,6 +110,304 @@ class _TimelinePostWidgetState extends State { } } +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: [ + if (post.creator != null) ...[ + InkWell( + onTap: onUserTap != null + ? () => onUserTap?.call(post.creator!.userId) + : null, + child: Row( + children: [ + if (post.creator!.imageUrl != null) ...[ + options.userAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + CircleAvatar( + radius: 14, + backgroundImage: CachedNetworkImageProvider( + post.creator!.imageUrl!, + ), + ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + const CircleAvatar( + radius: 14, + child: Icon( + Icons.person, + ), + ), + ], + const SizedBox(width: 10.0), + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: options.theme.textStyles.listPostCreatorTitleStyle ?? + theme.textTheme.titleSmall!.copyWith(color: Colors.black), + ), + ], + ), + ), + ], + const Spacer(), + if (allowDeletion) ...[ + PopupMenuButton( + onSelected: (value) async { + if (value == 'delete') { + await showPostDeletionConfirmationDialog( + options, + context, + onPostDelete, + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Text( + options.translations.deletePost, + style: options.theme.textStyles.deletePostStyle ?? + theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8.0), + options.theme.deleteIcon ?? + Icon( + Icons.delete, + color: options.theme.iconColor, + ), + ], + ), + ), + ], + child: options.theme.moreIcon ?? + Icon( + Icons.more_horiz_rounded, + color: options.theme.iconColor, + ), + ), + ], + ], + ); + } +} + +class _PostLikeAndReactionsInformation extends StatelessWidget { + const _PostLikeAndReactionsInformation({ + required this.service, + required this.options, + required this.userId, + required this.post, + required this.isLikedByUser, + required this.onTapComment, + }); + + final TimelineService service; + 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, + post, + ); + } else { + await service.postService.unlikePost( + userId, + 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; + }, + ) + : post.imageUrl != null + ? CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitWidth, + ) + : Image.memory( + width: double.infinity, + post.image!, + fit: BoxFit.fitWidth, + ), + ), + ); +} + +class _PostInfo extends StatelessWidget { + const _PostInfo({ + required this.options, + required this.post, + required this.onTap, + }); + + final TimelineOptions options; + final TimelinePost post; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Column( + children: [ + _PostLikeCountText( + post: post, + options: options, + ), + const SizedBox(height: 4.0), + Row( + children: [ + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: options.theme.textStyles.listCreatorNameStyle ?? + theme.textTheme.titleSmall!.copyWith( + color: Colors.black, + ), + ), + const SizedBox(width: 4.0), + Text( + post.title, + style: options.theme.textStyles.listPostTitleStyle ?? + theme.textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 4.0), + InkWell( + onTap: onTap, + child: Text( + options.translations.viewPost, + style: options.theme.textStyles.viewPostStyle ?? + theme.textTheme.titleSmall!.copyWith( + color: const Color(0xFF8D8D8D), + ), + ), + ), + ], + ); + } +} + class _PostLikeCountText extends StatelessWidget { const _PostLikeCountText({ required this.post, From 1e4da807e1807bae60a90cffe0657e3fc7df6bc8 Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Tue, 22 Apr 2025 13:23:52 +0200 Subject: [PATCH 4/4] refactor: share layout code where possible for posts --- CHANGELOG.md | 1 + .../lib/src/screens/timeline_post_screen.dart | 725 ++++++++---------- .../src/widgets/post_components/header.dart | 115 +++ .../src/widgets/post_components/image.dart | 75 ++ .../lib/src/widgets/post_components/info.dart | 44 ++ .../lib/src/widgets/timeline_post_widget.dart | 199 +---- 6 files changed, 566 insertions(+), 593 deletions(-) create mode 100644 packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart create mode 100644 packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart create mode 100644 packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cb830..ca7a474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Add minimal spacing between a post author and title in the post widget - Use listPostCreatorTitleStyle for post creator localizations when showing posts in a list +- Share more code between the various widgets within flutter_timeline_view ## 5.1.1 diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart index aca8e76..aa927cd 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_post_screen.dart @@ -9,9 +9,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/header.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/info.dart'; import 'package:flutter_timeline_view/src/widgets/reaction_bottom.dart'; -import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; -import 'package:flutter_timeline_view/src/widgets/timeline_post_widget.dart'; import 'package:intl/intl.dart'; class TimelinePostScreen extends StatefulWidget { @@ -172,204 +173,54 @@ class _TimelinePostScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - if (post.creator != null) - InkWell( - onTap: widget.onUserTap != null - ? () => - widget.onUserTap?.call(post.creator!.userId) - : null, - child: Row( - children: [ - if (post.creator!.imageUrl != null) ...[ - widget.options.userAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: - CachedNetworkImageProvider( - post.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10), - Text( - widget.options.nameBuilder - ?.call(post.creator) ?? - post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget.options.theme.textStyles - .listPostCreatorTitleStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - ], - ), - ), - const Spacer(), - if (!(widget.isOverviewScreen ?? false) && - (widget.allowAllDeletion || - post.creator?.userId == widget.userId)) ...[ - PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await showPostDeletionConfirmationDialog( - widget.options, - context, - widget.onPostDelete, - ); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Text( - widget.options.translations.deletePost, - style: widget.options.theme.textStyles - .deletePostStyle ?? - theme.textTheme.bodyMedium, - ), - const SizedBox(width: 8), - widget.options.theme.deleteIcon ?? - Icon( - Icons.delete, - color: widget.options.theme.iconColor, - ), - ], - ), - ), - ], - child: widget.options.theme.moreIcon ?? - Icon( - Icons.more_horiz_rounded, - color: widget.options.theme.iconColor, - ), - ), - ], - ], + PostHeader( + service: widget.service, + options: widget.options, + userId: widget.userId, + post: widget.post, + allowDeletion: !(widget.isOverviewScreen ?? false) && + (widget.allowAllDeletion || + post.creator?.userId == widget.userId), + onUserTap: widget.onUserTap, + onPostDelete: widget.onPostDelete, ), - // image of the posts if (post.imageUrl != null || post.image != null) ...[ - const SizedBox(height: 8), - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: widget.options.doubleTapTolike - ? TappableImage( - likeAndDislikeIcon: widget - .options.likeAndDislikeIconsForDoubleTap, - post: post, - userId: widget.userId, - onLike: ({required bool liked}) async { - var userId = widget.userId; - - late TimelinePost result; - - if (!liked) { - result = - await widget.service.postService.likePost( - userId, - post, - ); - } else { - result = await widget.service.postService - .unlikePost( - userId, - post, - ); - } - - await loadPostDetails(); - - return result.likedBy?.contains(userId) ?? - false; - }, - ) - : post.image != null - ? Image.memory( - width: double.infinity, - post.image!, - fit: BoxFit.fitHeight, - ) - : CachedNetworkImage( - width: double.infinity, - imageUrl: post.imageUrl!, - fit: BoxFit.fitHeight, - ), + const SizedBox(height: 8.0), + PostImage( + options: widget.options, + service: widget.service, + userId: widget.userId, + post: widget.post, + flexible: false, + onUpdatePost: loadPostDetails, ), ], - const SizedBox( - height: 8, - ), + const SizedBox(height: 8.0), // post information - Row( - children: [ - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - if (widget.isOverviewScreen ?? false) return; - if (isLikedByUser) { - updatePost( - await widget.service.postService.unlikePost( - widget.userId, - post, - ), - ); - setState(() {}); - } else { - updatePost( - await widget.service.postService.likePost( - widget.userId, - post, - ), - ); - setState(() {}); - } - }, - icon: isLikedByUser - ? widget.options.theme.likedIcon ?? - Icon( - Icons.favorite_rounded, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ) - : widget.options.theme.likeIcon ?? - Icon( - Icons.favorite_outline_outlined, - color: widget.options.theme.iconColor, - size: widget.options.iconSize, - ), - ), - const SizedBox(width: 8), - if (post.reactionEnabled) - 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, - ), - ], + _PostLikeAndReactionsInformation( + options: widget.options, + post: widget.post, + isLikedByUser: isLikedByUser, + onLikePressed: () async { + if (widget.isOverviewScreen ?? false) return; + if (isLikedByUser) { + updatePost( + await widget.service.postService.unlikePost( + widget.userId, + post, + ), + ); + setState(() {}); + } else { + updatePost( + await widget.service.postService.likePost( + widget.userId, + post, + ), + ); + setState(() {}); + } + }, ), const SizedBox(height: 8), // ignore: avoid_bool_literals_in_conditional_expressions @@ -385,24 +236,10 @@ class _TimelinePostScreenState extends State { ?.copyWith(color: Colors.black), ), ], - Text.rich( - TextSpan( - text: widget.options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - widget.options.translations.anonymousUser, - style: widget - .options.theme.textStyles.postCreatorNameStyle ?? - theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - children: [ - TextSpan( - text: post.title, - style: - widget.options.theme.textStyles.postTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], - ), + PostTitle( + options: widget.options, + post: post, + isForList: false, ), const SizedBox(height: 20), Text( @@ -420,198 +257,71 @@ class _TimelinePostScreenState extends State { if (post.reactionEnabled && widget.isOverviewScreen != null ? !widget.isOverviewScreen! : false) ...[ - Text( - widget.options.translations.commentsTitleOnPost, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - ), - for (var reaction - in post.reactions ?? []) ...[ - const SizedBox(height: 4), - GestureDetector( - onLongPressStart: (details) async { - if (reaction.creatorId == widget.userId || - widget.allowAllDeletion) { - var overlay = Overlay.of(context) - .context - .findRenderObject()! as RenderBox; - var position = RelativeRect.fromRect( - Rect.fromPoints( - details.globalPosition, - details.globalPosition, - ), - Offset.zero & overlay.size, - ); - // Show popup menu for deletion - var value = await showMenu( - context: context, - position: position, - items: [ - PopupMenuItem( - value: 'delete', - child: Text( - widget.options.translations.deleteReaction, - ), - ), - ], - ); - if (value == 'delete') { - // Call service to delete reaction - updatePost( - await widget.service.postService - .deletePostReaction(post, reaction.id), - ); - } - } - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (reaction.creator?.imageUrl != null && - reaction.creator!.imageUrl!.isNotEmpty) ...[ - widget.options.userAvatarBuilder?.call( - reaction.creator!, - 14, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: CachedNetworkImageProvider( - reaction.creator!.imageUrl!, - ), - ), - ] else ...[ - widget.options.anonymousAvatarBuilder?.call( - reaction.creator!, - 14, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10), - if (reaction.imageUrl != null) ...[ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.options.nameBuilder - ?.call(reaction.creator) ?? - reaction.creator?.fullName ?? - widget.options.translations - .anonymousUser, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: CachedNetworkImage( - imageUrl: reaction.imageUrl!, - fit: BoxFit.fitWidth, - ), - ), - ], - ), - ), - ] else ...[ - Expanded( - child: Text.rich( - TextSpan( - text: widget.options.nameBuilder - ?.call(reaction.creator) ?? - reaction.creator?.fullName ?? - widget - .options.translations.anonymousUser, - style: theme.textTheme.titleSmall! - .copyWith(color: Colors.black), - children: [ - const TextSpan(text: ' '), - TextSpan( - text: reaction.reaction ?? '', - style: theme.textTheme.bodySmall, - ), - const TextSpan(text: '\n'), - TextSpan( - text: dateFormat - .format(reaction.createdAt), - style: theme.textTheme.labelSmall! - .copyWith( - color: theme - .textTheme.labelSmall!.color! - .withOpacity(0.5), - letterSpacing: 0.5, - ), - ), - - // text should go to new line - ], - ), - ), - ), - ], - Builder( - builder: (context) { - var isLikedByUser = - reaction.likedBy?.contains(widget.userId) ?? - false; - return IconButton( - padding: const EdgeInsets.only(left: 12), - constraints: const BoxConstraints(), - onPressed: () async { - if (isLikedByUser) { - updatePost( - await widget.service.postService - .unlikeReaction( - widget.userId, - post, - reaction.id, - ), - ); - setState(() {}); - } else { - updatePost( - await widget.service.postService - .likeReaction( - widget.userId, - post, - reaction.id, - ), - ); - setState(() {}); - } - }, - icon: isLikedByUser - ? widget.options.theme.likedIcon ?? - Icon( - Icons.favorite_rounded, - color: - widget.options.theme.iconColor, - size: 14, - ) - : widget.options.theme.likeIcon ?? - Icon( - Icons.favorite_outline_outlined, - color: - widget.options.theme.iconColor, - size: 14, - ), - ); - }, + _CommentSection( + options: widget.options, + userId: widget.userId, + post: widget.post, + dateFormat: dateFormat, + onReactionLostPress: ( + LongPressStartDetails details, { + required TimelinePostReaction reaction, + }) async { + if (reaction.creatorId == widget.userId || + widget.allowAllDeletion) { + var overlay = Overlay.of(context) + .context + .findRenderObject()! as RenderBox; + var position = RelativeRect.fromRect( + Rect.fromPoints( + details.globalPosition, + details.globalPosition, ), - ], - ), - ), - const SizedBox(height: 4), - ], - if (post.reactions?.isEmpty ?? true) ...[ - Text( - widget.options.translations.firstComment, - style: theme.textTheme.bodySmall, - ), - ], + Offset.zero & overlay.size, + ); + // Show popup menu for deletion + var value = await showMenu( + context: context, + position: position, + items: [ + PopupMenuItem( + value: 'delete', + child: Text( + widget.options.translations.deleteReaction, + ), + ), + ], + ); + if (value == 'delete') { + // Call service to delete reaction + updatePost( + await widget.service.postService + .deletePostReaction(post, reaction.id), + ); + } + } + }, + onLikeReaction: (TimelinePostReaction reaction) async { + if (isLikedByUser) { + updatePost( + await widget.service.postService.unlikeReaction( + widget.userId, + post, + reaction.id, + ), + ); + setState(() {}); + } else { + updatePost( + await widget.service.postService.likeReaction( + widget.userId, + post, + reaction.id, + ), + ); + setState(() {}); + } + }, + ), const SizedBox(height: 120), ], ], @@ -693,3 +403,210 @@ class _TimelinePostScreenState extends State { ); } } + +class _PostLikeAndReactionsInformation extends StatelessWidget { + const _PostLikeAndReactionsInformation({ + required this.options, + required this.post, + required this.isLikedByUser, + required this.onLikePressed, + }); + + final TimelineOptions options; + final TimelinePost post; + final bool isLikedByUser; + final VoidCallback onLikePressed; + + @override + Widget build(BuildContext context) => Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: onLikePressed, + icon: isLikedByUser + ? options.theme.likedIcon ?? + Icon( + Icons.favorite_rounded, + color: options.theme.iconColor, + size: options.iconSize, + ) + : options.theme.likeIcon ?? + Icon( + Icons.favorite_outline_outlined, + color: options.theme.iconColor, + size: options.iconSize, + ), + ), + const SizedBox(width: 8.0), + if (post.reactionEnabled) + 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, + ), + ], + ); +} + +class _CommentSection extends StatelessWidget { + const _CommentSection({ + required this.options, + required this.userId, + required this.post, + required this.dateFormat, + required this.onReactionLostPress, + required this.onLikeReaction, + }); + + final TimelineOptions options; + final String userId; + final TimelinePost post; + final DateFormat dateFormat; + + final void Function( + LongPressStartDetails details, { + required TimelinePostReaction reaction, + }) onReactionLostPress; + final void Function(TimelinePostReaction reaction) onLikeReaction; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Column( + children: [ + Text( + options.translations.commentsTitleOnPost, + style: theme.textTheme.titleSmall!.copyWith(color: Colors.black), + ), + for (var reaction in post.reactions ?? []) ...[ + const SizedBox(height: 4), + GestureDetector( + onLongPressStart: (details) async { + onReactionLostPress(details, reaction: reaction); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (reaction.creator?.imageUrl != null && + reaction.creator!.imageUrl!.isNotEmpty) ...[ + options.userAvatarBuilder?.call( + reaction.creator!, + 14, + ) ?? + CircleAvatar( + radius: 14, + backgroundImage: CachedNetworkImageProvider( + reaction.creator!.imageUrl!, + ), + ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + reaction.creator!, + 14, + ) ?? + const CircleAvatar( + radius: 14, + child: Icon( + Icons.person, + ), + ), + ], + const SizedBox(width: 10), + if (reaction.imageUrl != null) ...[ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + options.nameBuilder?.call(reaction.creator) ?? + reaction.creator?.fullName ?? + options.translations.anonymousUser, + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.black), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: CachedNetworkImage( + imageUrl: reaction.imageUrl!, + fit: BoxFit.fitWidth, + ), + ), + ], + ), + ), + ] else ...[ + Expanded( + child: Text.rich( + TextSpan( + text: options.nameBuilder?.call(reaction.creator) ?? + reaction.creator?.fullName ?? + options.translations.anonymousUser, + style: theme.textTheme.titleSmall! + .copyWith(color: Colors.black), + children: [ + const TextSpan(text: ' '), + TextSpan( + text: reaction.reaction ?? '', + style: theme.textTheme.bodySmall, + ), + const TextSpan(text: '\n'), + TextSpan( + text: dateFormat.format(reaction.createdAt), + style: theme.textTheme.labelSmall!.copyWith( + color: theme.textTheme.labelSmall!.color! + .withOpacity(0.5), + letterSpacing: 0.5, + ), + ), + + // text should go to new line + ], + ), + ), + ), + ], + Builder( + builder: (context) { + var isLikedByUser = + reaction.likedBy?.contains(userId) ?? false; + return IconButton( + padding: const EdgeInsets.only(left: 12), + constraints: const BoxConstraints(), + onPressed: () => onLikeReaction(reaction), + icon: isLikedByUser + ? options.theme.likedIcon ?? + Icon( + Icons.favorite_rounded, + color: options.theme.iconColor, + size: 14, + ) + : options.theme.likeIcon ?? + Icon( + Icons.favorite_outline_outlined, + color: options.theme.iconColor, + size: 14, + ), + ); + }, + ), + ], + ), + ), + const SizedBox(height: 4), + ], + if (post.reactions?.isEmpty ?? true) ...[ + Text( + options.translations.firstComment, + style: theme.textTheme.bodySmall, + ), + ], + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart new file mode 100644 index 0000000..0c30a5c --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/header.dart @@ -0,0 +1,115 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; + +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, + super.key, + }); + + 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: [ + if (post.creator != null) ...[ + InkWell( + onTap: onUserTap != null + ? () => onUserTap?.call(post.creator!.userId) + : null, + child: Row( + children: [ + if (post.creator!.imageUrl != null) ...[ + options.userAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + CircleAvatar( + radius: 14, + backgroundImage: + CachedNetworkImageProvider(post.creator!.imageUrl!), + ), + ] else ...[ + options.anonymousAvatarBuilder?.call( + post.creator!, + 28, + ) ?? + const CircleAvatar( + radius: 14, + child: Icon( + Icons.person, + ), + ), + ], + const SizedBox(width: 10.0), + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: options.theme.textStyles.listPostCreatorTitleStyle ?? + theme.textTheme.titleSmall!.copyWith(color: Colors.black), + ), + ], + ), + ), + ], + const Spacer(), + if (allowDeletion) ...[ + PopupMenuButton( + onSelected: (value) async { + if (value == 'delete') { + await showPostDeletionConfirmationDialog( + options, + context, + onPostDelete, + ); + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Text( + options.translations.deletePost, + style: options.theme.textStyles.deletePostStyle ?? + theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8.0), + options.theme.deleteIcon ?? + Icon( + Icons.delete, + color: options.theme.iconColor, + ), + ], + ), + ), + ], + child: options.theme.moreIcon ?? + Icon( + Icons.more_horiz_rounded, + color: options.theme.iconColor, + ), + ), + ], + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart new file mode 100644 index 0000000..6228dec --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/image.dart @@ -0,0 +1,75 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; +import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; + +class PostImage extends StatelessWidget { + const PostImage({ + required this.options, + required this.service, + required this.userId, + required this.post, + this.flexible = true, + this.onUpdatePost, + super.key, + }); + + final TimelineOptions options; + final TimelineService service; + final String userId; + final TimelinePost post; + final bool flexible; + + final VoidCallback? onUpdatePost; + + @override + Widget build(BuildContext context) { + var body = 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, + ); + } + + onUpdatePost?.call(); + + return result.likedBy?.contains(userId) ?? false; + }, + ) + : post.imageUrl != null + ? CachedNetworkImage( + width: double.infinity, + imageUrl: post.imageUrl!, + fit: BoxFit.fitWidth, + ) + : Image.memory( + width: double.infinity, + post.image!, + fit: BoxFit.fitWidth, + ), + ); + + if (!flexible) return body; + + return Flexible( + flex: options.postWidgetHeight != null ? 1 : 0, + child: body, + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart b/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart new file mode 100644 index 0000000..3194468 --- /dev/null +++ b/packages/flutter_timeline_view/lib/src/widgets/post_components/info.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; +import 'package:flutter_timeline_view/flutter_timeline_view.dart'; + +class PostTitle extends StatelessWidget { + const PostTitle({ + required this.options, + required this.post, + this.isForList = false, + super.key, + }); + + final TimelineOptions options; + final TimelinePost post; + + final bool isForList; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var creatorNameStyle = (isForList + ? options.theme.textStyles.listCreatorNameStyle + : options.theme.textStyles.postCreatorNameStyle) ?? + theme.textTheme.titleSmall?.copyWith(color: Colors.black); + + return Row( + children: [ + Text( + options.nameBuilder?.call(post.creator) ?? + post.creator?.fullName ?? + options.translations.anonymousUser, + style: creatorNameStyle, + ), + const SizedBox(width: 4.0), + Text( + post.title, + style: options.theme.textStyles.listPostTitleStyle ?? + theme.textTheme.bodySmall, + ), + ], + ); + } +} diff --git a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart index e6b35f3..562290f 100644 --- a/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart +++ b/packages/flutter_timeline_view/lib/src/widgets/timeline_post_widget.dart @@ -2,13 +2,14 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_timeline_interface/flutter_timeline_interface.dart'; import 'package:flutter_timeline_view/src/config/timeline_options.dart'; import 'package:flutter_timeline_view/src/widgets/default_filled_button.dart'; -import 'package:flutter_timeline_view/src/widgets/tappable_image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/header.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/image.dart'; +import 'package:flutter_timeline_view/src/widgets/post_components/info.dart'; class TimelinePostWidget extends StatefulWidget { const TimelinePostWidget({ @@ -63,7 +64,7 @@ class _TimelinePostWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _PostHeader( + PostHeader( service: widget.service, options: widget.options, userId: widget.userId, @@ -75,7 +76,7 @@ class _TimelinePostWidgetState extends State { ), if (widget.post.imageUrl != null || widget.post.image != null) ...[ const SizedBox(height: 8.0), - _PostImage( + PostImage( service: widget.service, options: widget.options, userId: widget.userId, @@ -110,117 +111,6 @@ class _TimelinePostWidgetState extends State { } } -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: [ - if (post.creator != null) ...[ - InkWell( - onTap: onUserTap != null - ? () => onUserTap?.call(post.creator!.userId) - : null, - child: Row( - children: [ - if (post.creator!.imageUrl != null) ...[ - options.userAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - CircleAvatar( - radius: 14, - backgroundImage: CachedNetworkImageProvider( - post.creator!.imageUrl!, - ), - ), - ] else ...[ - options.anonymousAvatarBuilder?.call( - post.creator!, - 28, - ) ?? - const CircleAvatar( - radius: 14, - child: Icon( - Icons.person, - ), - ), - ], - const SizedBox(width: 10.0), - Text( - options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - options.translations.anonymousUser, - style: options.theme.textStyles.listPostCreatorTitleStyle ?? - theme.textTheme.titleSmall!.copyWith(color: Colors.black), - ), - ], - ), - ), - ], - const Spacer(), - if (allowDeletion) ...[ - PopupMenuButton( - onSelected: (value) async { - if (value == 'delete') { - await showPostDeletionConfirmationDialog( - options, - context, - onPostDelete, - ); - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Text( - options.translations.deletePost, - style: options.theme.textStyles.deletePostStyle ?? - theme.textTheme.bodyMedium, - ), - const SizedBox(width: 8.0), - options.theme.deleteIcon ?? - Icon( - Icons.delete, - color: options.theme.iconColor, - ), - ], - ), - ), - ], - child: options.theme.moreIcon ?? - Icon( - Icons.more_horiz_rounded, - color: options.theme.iconColor, - ), - ), - ], - ], - ); - } -} - class _PostLikeAndReactionsInformation extends StatelessWidget { const _PostLikeAndReactionsInformation({ required this.service, @@ -295,62 +185,6 @@ class _PostLikeAndReactionsInformation extends StatelessWidget { ); } -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; - }, - ) - : post.imageUrl != null - ? CachedNetworkImage( - width: double.infinity, - imageUrl: post.imageUrl!, - fit: BoxFit.fitWidth, - ) - : Image.memory( - width: double.infinity, - post.image!, - fit: BoxFit.fitWidth, - ), - ), - ); -} - class _PostInfo extends StatelessWidget { const _PostInfo({ required this.options, @@ -367,30 +201,17 @@ class _PostInfo extends StatelessWidget { var theme = Theme.of(context); return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ _PostLikeCountText( post: post, options: options, ), const SizedBox(height: 4.0), - Row( - children: [ - Text( - options.nameBuilder?.call(post.creator) ?? - post.creator?.fullName ?? - options.translations.anonymousUser, - style: options.theme.textStyles.listCreatorNameStyle ?? - theme.textTheme.titleSmall!.copyWith( - color: Colors.black, - ), - ), - const SizedBox(width: 4.0), - Text( - post.title, - style: options.theme.textStyles.listPostTitleStyle ?? - theme.textTheme.bodySmall, - ), - ], + PostTitle( + options: options, + post: post, + isForList: true, ), const SizedBox(height: 4.0), InkWell(