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
- 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_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<TimelinePostScreen> {
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
.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,
),
),
],
],
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<TimelinePostScreen> {
?.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<TimelinePostScreen> {
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 ?? <TimelinePostReaction>[]) ...[
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<String>(
context: context,
position: position,
items: [
PopupMenuItem<String>(
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<String>(
context: context,
position: position,
items: [
PopupMenuItem<String>(
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<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
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({
@ -53,7 +54,6 @@ class TimelinePostWidget extends StatefulWidget {
class _TimelinePostWidgetState extends State<TimelinePostWidget> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var isLikedByUser = widget.post.likedBy?.contains(widget.userId) ?? false;
return SizedBox(
@ -64,300 +64,171 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
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),
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,
),
),
],
],
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),
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,
),
),
),
],
const SizedBox(
height: 8,
),
// 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,
),
Text('${widget.post.likes}'),
if (widget.post.reactionEnabled) ...[
const SizedBox(
width: 8,
),
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,
),
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),
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(
post: widget.post,
const SizedBox(height: 8.0),
PostImage(
service: widget.service,
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),
InkWell(
onTap: widget.onTap,
child: Text(
widget.options.translations.viewPost,
style: widget.options.theme.textStyles.viewPostStyle ??
theme.textTheme.titleSmall!.copyWith(
color: const Color(0xFF8D8D8D),
),
),
userId: widget.userId,
post: widget.post,
),
],
if (widget.options.dividerBuilder != null)
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 _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 _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: [
_PostLikeCountText(
post: post,
options: options,
),
const SizedBox(height: 4.0),
PostTitle(
options: options,
post: post,
isForList: true,
),
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,