import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:intl/intl.dart'; import 'package:solian/models/post.dart'; import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/shells/title_shell.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/posts/post_tags.dart'; import 'package:solian/widgets/posts/post_quick_action.dart'; import 'package:solian/widgets/sized_container.dart'; import 'package:timeago/timeago.dart' show format; class PostItem extends StatefulWidget { final Post item; final bool isClickable; final bool isCompact; final bool isReactable; final bool isShowReply; final bool isShowEmbed; final bool isFullDate; final bool isContentSelectable; final String? attachmentParent; final Color? backgroundColor; const PostItem({ super.key, required this.item, this.isClickable = false, this.isCompact = false, this.isReactable = true, this.isShowReply = true, this.isShowEmbed = true, this.isFullDate = false, this.isContentSelectable = false, this.attachmentParent, this.backgroundColor, }); @override State createState() => _PostItemState(); } class _PostItemState extends State { late final Post item; Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); @override void initState() { item = widget.item; super.initState(); } Widget _buildDate() { if (widget.isFullDate) { return Text(DateFormat('y/M/d HH:mm') .format(item.publishedAt?.toLocal() ?? DateTime.now())); } else { return Text( format( item.publishedAt?.toLocal() ?? DateTime.now(), locale: 'en_short', ), ); } } Widget _buildHeader() { return Row( children: [ if (widget.isCompact) AccountAvatar( content: item.author.avatar.toString(), radius: 10, ).paddingOnly(left: 2), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( item.author.nick, style: const TextStyle(fontWeight: FontWeight.bold), ), _buildDate().paddingOnly(left: 4), ], ), if (item.body['title'] != null) Text( item.body['title'], style: Theme.of(context) .textTheme .bodyMedium! .copyWith(fontSize: 15), ), if (item.body['description'] != null) Text( item.body['description'], style: Theme.of(context).textTheme.bodySmall, ), if (item.body['description'] != null || item.body['title'] != null) const Divider(thickness: 0.3, height: 1).paddingSymmetric( vertical: 8, ), ], ).paddingOnly(left: widget.isCompact ? 6 : 12), ), ], ); } Widget _buildFooter() { List labels = List.empty(growable: true); if (widget.item.editedAt != null) { labels.add('postEdited'.trParams({ 'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()), })); } if (widget.item.realm != null) { labels.add('postInRealm'.trParams({ 'realm': widget.item.realm!.alias, })); } List widgets = List.empty(growable: true); if (widget.item.tags?.isNotEmpty ?? false) { widgets.add(PostTagsList(tags: widget.item.tags!)); } if (labels.isNotEmpty) { widgets.add(Text( labels.join(' ยท '), textAlign: TextAlign.left, style: TextStyle( fontSize: 12, color: _unFocusColor, ), )); } if (widget.item.pinnedAt != null) { widgets.add(Text( 'postPinned'.tr, style: TextStyle(fontSize: 12, color: _unFocusColor), )); } if (widgets.isEmpty) { return const SizedBox(); } else { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgets, ).paddingOnly(top: 4); } } Widget _buildReply(BuildContext context) { return OpenContainer( closedBuilder: (_, openContainer) => Column( children: [ Row( children: [ FaIcon( FontAwesomeIcons.reply, size: 16, color: _unFocusColor, ), Expanded( child: Text( 'postRepliedNotify'.trParams( {'username': '@${widget.item.replyTo!.author.name}'}, ), style: TextStyle(color: _unFocusColor), ).paddingOnly(left: 6), ), ], ).paddingOnly(left: 12), Card( elevation: 1, child: PostItem( item: widget.item.replyTo!, isCompact: true, attachmentParent: widget.item.id.toString(), ).paddingSymmetric(vertical: 8), ), ], ), openBuilder: (_, __) => TitleShell( title: 'postDetail'.tr, child: PostDetailScreen( id: widget.item.replyTo!.id.toString(), post: widget.item.replyTo!, ), ), closedElevation: 0, openElevation: 0, closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface, ); } Widget _buildRepost(BuildContext context) { return OpenContainer( closedBuilder: (_, openContainer) => Column( children: [ Row( children: [ FaIcon( FontAwesomeIcons.retweet, size: 16, color: _unFocusColor, ), Expanded( child: Text( 'postRepostedNotify'.trParams( {'username': '@${widget.item.repostTo!.author.name}'}, ), style: TextStyle(color: _unFocusColor), ).paddingOnly(left: 6), ), ], ).paddingOnly(left: 12), Card( elevation: 1, child: PostItem( item: widget.item.repostTo!, isCompact: true, attachmentParent: widget.item.id.toString(), ).paddingSymmetric(vertical: 8), ), ], ), openBuilder: (_, __) => TitleShell( title: 'postDetail'.tr, child: PostDetailScreen( id: widget.item.repostTo!.id.toString(), post: widget.item.repostTo!, ), ), closedElevation: 0, openElevation: 0, closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface, ); } @override Widget build(BuildContext context) { final List attachments = item.body['attachments'] is List ? item.body['attachments']?.cast() : List.empty(); final hasAttachment = attachments.isNotEmpty; if (widget.isCompact) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader().paddingSymmetric(horizontal: 12), MarkdownTextContent( content: item.body['content'], isSelectable: widget.isContentSelectable, ).paddingOnly( left: 16, right: 12, top: 2, bottom: hasAttachment ? 4 : 0, ), _buildFooter().paddingOnly(left: 16), if (attachments.isNotEmpty) Row( children: [ Icon( Icons.attachment, size: 18, color: _unFocusColor, ).paddingOnly(right: 6), Text( 'attachmentHint'.trParams( {'count': attachments.length.toString()}, ), style: TextStyle(color: _unFocusColor), ) ], ).paddingOnly(left: 16, top: 4), ], ); } return OpenContainer( closedBuilder: (_, openContainer) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( child: AccountAvatar(content: item.author.avatar.toString()), onTap: () { showModalBottomSheet( useRootNavigator: true, isScrollControlled: true, backgroundColor: Theme.of(context).colorScheme.surface, context: context, builder: (context) => AccountProfilePopup( account: item.author, ), ); }, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), SizedContainer( maxWidth: 640, child: MarkdownTextContent( content: item.body['content'], isSelectable: widget.isContentSelectable, ).paddingOnly(left: 12, right: 8), ), if (widget.item.replyTo != null && widget.isShowEmbed) _buildReply(context).paddingOnly(top: 4), if (widget.item.repostTo != null && widget.isShowEmbed) _buildRepost(context).paddingOnly(top: 4), _buildFooter().paddingOnly(left: 12), ], ), ), ], ).paddingOnly( top: 10, bottom: hasAttachment ? 10 : 0, right: 16, left: 16, ), AttachmentList( parentId: widget.item.id.toString(), attachmentsId: attachments, isGrid: attachments.length > 1, ), if (widget.isShowReply && widget.isReactable) PostQuickAction( isShowReply: widget.isShowReply, isReactable: widget.isReactable, item: widget.item, onReact: (symbol, changes) { setState(() { item.metric!.reactionList[symbol] = (item.metric!.reactionList[symbol] ?? 0) + changes; }); }, ).paddingOnly( top: hasAttachment ? 10 : 6, left: hasAttachment ? 24 : 60, right: 16, bottom: 10, ) else const SizedBox(height: 10), ], ), openBuilder: (_, __) => TitleShell( title: 'postDetail'.tr, child: PostDetailScreen( id: item.id.toString(), post: item, ), ), closedElevation: 0, openElevation: 0, closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface, ); } }