This commit is contained in:
Bart Ribbers 2025-04-30 08:11:19 +00:00 committed by GitHub
commit 1e231adde7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 717 additions and 689 deletions

View file

@ -1,3 +1,9 @@
## 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
- Share more code between the various widgets within flutter_timeline_view
## 5.1.1 ## 5.1.1
- Be honest about which Dart and Flutter versions we support - Be honest about which Dart and Flutter versions we support

View file

@ -9,9 +9,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.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/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/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'; import 'package:intl/intl.dart';
class TimelinePostScreen extends StatefulWidget { class TimelinePostScreen extends StatefulWidget {
@ -172,159 +173,35 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( PostHeader(
children: [ service: widget.service,
if (post.creator != null) options: widget.options,
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
.postCreatorTitleStyle ??
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) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
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,
),
),
],
],
),
// 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, userId: widget.userId,
onLike: ({required bool liked}) async { post: widget.post,
var userId = widget.userId; allowDeletion: !(widget.isOverviewScreen ?? false) &&
(widget.allowAllDeletion ||
late TimelinePost result; post.creator?.userId == widget.userId),
onUserTap: widget.onUserTap,
if (!liked) { onPostDelete: widget.onPostDelete,
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,
), ),
if (post.imageUrl != null || post.image != null) ...[
const SizedBox(height: 8.0),
PostImage(
options: widget.options,
service: widget.service,
userId: widget.userId,
post: widget.post,
flexible: false,
onUpdatePost: loadPostDetails,
), ),
], ],
const SizedBox( const SizedBox(height: 8.0),
height: 8,
),
// post information // post information
Row( _PostLikeAndReactionsInformation(
children: [ options: widget.options,
IconButton( post: widget.post,
padding: EdgeInsets.zero, isLikedByUser: isLikedByUser,
constraints: const BoxConstraints(), onLikePressed: () async {
onPressed: () async {
if (widget.isOverviewScreen ?? false) return; if (widget.isOverviewScreen ?? false) return;
if (isLikedByUser) { if (isLikedByUser) {
updatePost( updatePost(
@ -344,32 +221,6 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
setState(() {}); 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,
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// ignore: avoid_bool_literals_in_conditional_expressions // ignore: avoid_bool_literals_in_conditional_expressions
@ -385,24 +236,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
?.copyWith(color: Colors.black), ?.copyWith(color: Colors.black),
), ),
], ],
Text.rich( PostTitle(
TextSpan( options: widget.options,
text: widget.options.nameBuilder?.call(post.creator) ?? post: post,
post.creator?.fullName ?? isForList: false,
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,
),
],
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
@ -420,16 +257,15 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
if (post.reactionEnabled && widget.isOverviewScreen != null if (post.reactionEnabled && widget.isOverviewScreen != null
? !widget.isOverviewScreen! ? !widget.isOverviewScreen!
: false) ...[ : false) ...[
Text( _CommentSection(
widget.options.translations.commentsTitleOnPost, options: widget.options,
style: theme.textTheme.titleSmall! userId: widget.userId,
.copyWith(color: Colors.black), post: widget.post,
), dateFormat: dateFormat,
for (var reaction onReactionLostPress: (
in post.reactions ?? <TimelinePostReaction>[]) ...[ LongPressStartDetails details, {
const SizedBox(height: 4), required TimelinePostReaction reaction,
GestureDetector( }) async {
onLongPressStart: (details) async {
if (reaction.creatorId == widget.userId || if (reaction.creatorId == widget.userId ||
widget.allowAllDeletion) { widget.allowAllDeletion) {
var overlay = Overlay.of(context) var overlay = Overlay.of(context)
@ -464,107 +300,10 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
} }
} }
}, },
child: Row( onLikeReaction: (TimelinePostReaction reaction) async {
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) { if (isLikedByUser) {
updatePost( updatePost(
await widget.service.postService await widget.service.postService.unlikeReaction(
.unlikeReaction(
widget.userId, widget.userId,
post, post,
reaction.id, reaction.id,
@ -573,8 +312,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
setState(() {}); setState(() {});
} else { } else {
updatePost( updatePost(
await widget.service.postService await widget.service.postService.likeReaction(
.likeReaction(
widget.userId, widget.userId,
post, post,
reaction.id, reaction.id,
@ -583,35 +321,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
setState(() {}); 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,
), ),
);
},
),
],
),
),
const SizedBox(height: 4),
],
if (post.reactions?.isEmpty ?? true) ...[
Text(
widget.options.translations.firstComment,
style: theme.textTheme.bodySmall,
),
],
const SizedBox(height: 120), const SizedBox(height: 120),
], ],
], ],
@ -693,3 +403,210 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
); );
} }
} }
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 ?? <TimelinePostReaction>[]) ...[
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,
),
],
],
);
}
}

View file

@ -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) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
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,
),
),
],
],
);
}
}

View file

@ -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,
);
}
}

View file

@ -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,
),
],
);
}
}

View file

@ -2,13 +2,14 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.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/config/timeline_options.dart';
import 'package:flutter_timeline_view/src/widgets/default_filled_button.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 { class TimelinePostWidget extends StatefulWidget {
const TimelinePostWidget({ const TimelinePostWidget({
@ -53,7 +54,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,296 +64,167 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( PostHeader(
children: [ service: widget.service,
if (widget.post.creator != null) ...[ options: widget.options,
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),
Text(
widget.options.nameBuilder?.call(widget.post.creator) ??
widget.post.creator?.fullName ??
widget.options.translations.anonymousUser,
style: widget.options.theme.textStyles
.postCreatorTitleStyle ??
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) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
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,
),
),
],
],
),
// image of the post
if (widget.post.imageUrl != null || widget.post.image != null) ...[
const SizedBox(height: 8),
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, userId: widget.userId,
onLike: ({required bool liked}) async { post: widget.post,
var userId = widget.userId; allowDeletion: widget.allowAllDeletion ||
widget.post.creator?.userId == widget.userId,
late TimelinePost result; onUserTap: widget.onUserTap,
onPostDelete: widget.onPostDelete,
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,
),
), ),
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( const SizedBox(height: 8.0),
height: 8, _PostLikeAndReactionsInformation(
service: widget.service,
options: widget.options,
userId: widget.userId,
post: widget.post,
isLikedByUser: isLikedByUser,
onTapComment: widget.onTap,
), ),
// post information const SizedBox(height: 8.0),
if (widget.options.iconsWithValues) ...[ if (widget.options.itemInfoBuilder != null) ...[
Row( 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 _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: [ children: [
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () async { onPressed: () async {
var userId = widget.userId;
if (!isLikedByUser) { if (!isLikedByUser) {
await widget.service.postService.likePost( await service.postService.likePost(
userId, userId,
widget.post, post,
); );
} else { } else {
await widget.service.postService.unlikePost( await service.postService.unlikePost(
userId, userId,
widget.post, post,
); );
} }
}, },
icon: widget.options.theme.likeIcon ?? icon: options.theme.likeIcon ??
Icon( Icon(
isLikedByUser isLikedByUser
? Icons.favorite_rounded ? Icons.favorite_rounded
: Icons.favorite_outline_outlined, : Icons.favorite_outline_outlined,
color: widget.options.theme.iconColor, color: options.theme.iconColor,
size: widget.options.iconSize, size: options.iconSize,
), ),
), ),
const SizedBox( const SizedBox(width: 4.0),
width: 4, if (options.iconsWithValues) ...[
), Text('${post.likes}'),
Text('${widget.post.likes}'), ],
if (widget.post.reactionEnabled) ...[ if (post.reactionEnabled) ...[
const SizedBox( const SizedBox(width: 8.0),
width: 8,
),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: widget.onTap, onPressed: onTapComment,
icon: widget.options.theme.commentIcon ?? icon: options.theme.commentIcon ??
SvgPicture.asset( SvgPicture.asset(
'assets/Comment.svg', 'assets/Comment.svg',
package: 'flutter_timeline_view', package: 'flutter_timeline_view',
// ignore: deprecated_member_use // ignore: deprecated_member_use
color: widget.options.theme.iconColor, color: options.theme.iconColor,
width: widget.options.iconSize, width: options.iconSize,
height: widget.options.iconSize, height: options.iconSize,
), ),
), ),
const SizedBox( if (options.iconsWithValues) ...[
width: 4, const SizedBox(width: 4.0),
), Text('${post.reaction}'),
Text('${widget.post.reaction}'),
], ],
], ],
), ],
] else ...[ );
Row( }
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(
crossAxisAlignment: CrossAxisAlignment.start,
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),
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,
), ),
Text.rich( const SizedBox(height: 4.0),
TextSpan( PostTitle(
text: widget.options.nameBuilder?.call(widget.post.creator) ?? options: options,
widget.post.creator?.fullName ?? post: post,
widget.options.translations.anonymousUser, isForList: true,
style: widget.options.theme.textStyles.listCreatorNameStyle ??
theme.textTheme.titleSmall!.copyWith(
color: Colors.black,
), ),
children: [ const SizedBox(height: 4.0),
TextSpan(
text: widget.post.title,
style: widget.options.theme.textStyles.listPostTitleStyle ??
theme.textTheme.bodySmall,
),
],
),
),
const SizedBox(height: 4),
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!(),
],
),
); );
} }
} }