post_container.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
  3. import 'package:url_launcher/url_launcher.dart';
  4. import 'package:veloe_kemono_party_flutter/models/attachment.dart';
  5. import 'package:veloe_kemono_party_flutter/pages/media_carousel_screen';
  6. import 'package:veloe_kemono_party_flutter/pages/widgets/video_preview.dart';
  7. import 'smart_image_container.dart';
  8. import 'package:share_plus/share_plus.dart';
  9. import '../../models/post.dart';
  10. class PostContainer extends StatelessWidget {
  11. final Post post;
  12. final int postIndex; // The index of this post in the list of posts (in PostsScreen)
  13. final List<Post> allPosts;
  14. final Future<void> Function() loadMorePosts;
  15. const PostContainer({
  16. super.key,
  17. required this.post,
  18. required this.postIndex,
  19. required this.allPosts,
  20. required this.loadMorePosts,
  21. });
  22. @override
  23. Widget build(BuildContext context) {
  24. return Card(
  25. margin: const EdgeInsets.all(8),
  26. child: Padding(
  27. padding: const EdgeInsets.all(16),
  28. child: Column(
  29. mainAxisSize: MainAxisSize.min,
  30. crossAxisAlignment: CrossAxisAlignment.start,
  31. children: [
  32. _PostHeader(post: post),
  33. _buildContent(context, post.attachments),
  34. //_PostContentSection(post: post),
  35. _PostActionsFooter(post: post),
  36. ],
  37. ),
  38. ),
  39. );
  40. }
  41. Widget _buildContent(BuildContext context, List<Attachment> attachments) {
  42. final imageAttachments = post.attachments
  43. .where((a) => a.isImage || a.isVideo)
  44. .toList();
  45. return Column(
  46. mainAxisSize: MainAxisSize.min,
  47. crossAxisAlignment: CrossAxisAlignment.start,
  48. children: [
  49. // Text Content
  50. HtmlWidget(
  51. post.content,
  52. textStyle: Theme.of(context).textTheme.bodyMedium,
  53. onTapUrl: (url) => _handleUrlLaunch(context, url),
  54. ),
  55. // Attachments Grid
  56. if (imageAttachments.isNotEmpty)
  57. LayoutBuilder(
  58. builder: (context, constraints) {
  59. return Column(
  60. children: [
  61. const SizedBox(height: 12),
  62. GridView.builder(
  63. shrinkWrap: true,
  64. physics: const NeverScrollableScrollPhysics(),
  65. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  66. crossAxisCount: _calculateColumnCount(constraints.maxWidth),
  67. ),
  68. itemCount: imageAttachments.length,
  69. itemBuilder: (context, index) => Padding(
  70. padding: const EdgeInsets.all(4),
  71. child:
  72. _buildMediaPreview(context, imageAttachments[index], index, attachments)
  73. /*SmartImageContainer(
  74. imageUrl: imageAttachments[index].link,
  75. //onTap: () => _handleAttachmentTap(index),
  76. ),*/
  77. ),
  78. ),
  79. // Attachment links list
  80. ...post.attachments.map(
  81. (attachment) => _buildAttachmentLink(context,attachment),
  82. ),
  83. ],
  84. );
  85. },
  86. ),
  87. ],
  88. );
  89. }
  90. Widget _buildAttachmentLink(BuildContext context,Attachment attachment) {
  91. return Padding(
  92. padding: const EdgeInsets.only(top: 8),
  93. child: InkWell(
  94. onTap: () => _handleUrlLaunch(context, attachment.link),
  95. child: Row(
  96. children: [
  97. const Icon(Icons.link, size: 16),
  98. const SizedBox(width: 8),
  99. Expanded(
  100. child: Text(
  101. attachment.name,
  102. style: const TextStyle(
  103. color: Colors.blue,
  104. decoration: TextDecoration.underline,
  105. ),
  106. overflow: TextOverflow.ellipsis,
  107. ),
  108. ),
  109. ],
  110. ),
  111. ),
  112. );
  113. }
  114. int _calculateColumnCount(double availableWidth) {
  115. return (availableWidth / 400).floor().clamp(1, 3);
  116. }
  117. Widget _buildMediaPreview(BuildContext context,Attachment attachment, int index, List<Attachment> attachments) {
  118. return GestureDetector(
  119. onTap: () {
  120. _openMediaCarousel(context, index);
  121. },
  122. child: attachment.isVideo
  123. ? VideoPreview(videoUrl: attachment.link, onTap: () => _openMediaCarousel(context, index),)
  124. : (attachment.isImage ? SmartImageContainer(imageUrl: attachment.link) : null),
  125. );
  126. }
  127. void _openMediaCarousel(BuildContext context, int attachmentIndex) {
  128. List<MediaCarouselItem> mediaItems = [];
  129. for (int i = 0; i < allPosts.length; i++) {
  130. final post = allPosts[i];
  131. for (int j = 0; j < post.attachments.length; j++) {
  132. final attachment = post.attachments[j];
  133. mediaItems.add(
  134. MediaCarouselItem(
  135. url: attachment.link,
  136. type: attachment.isVideo ? "video" : "image",
  137. postIndex: i,
  138. attachmentIndex: j,
  139. ),
  140. );
  141. }
  142. }
  143. int globalIndex = 0;
  144. for (int i = 0; i < postIndex; i++) {
  145. globalIndex += allPosts[i].attachments.length;
  146. }
  147. globalIndex += attachmentIndex;
  148. Navigator.push(
  149. context,
  150. MaterialPageRoute(
  151. builder: (context) => MediaCarouselScreen(
  152. initialIndex: globalIndex,
  153. mediaItems: mediaItems,
  154. loadMorePosts: loadMorePosts,
  155. allPosts: allPosts,
  156. ),
  157. ),
  158. );
  159. }
  160. }
  161. class _PostHeader extends StatelessWidget {
  162. final Post post;
  163. const _PostHeader({required this.post});
  164. @override
  165. Widget build(BuildContext context) {
  166. return Padding(
  167. padding: const EdgeInsets.only(bottom: 12),
  168. child: Row(
  169. children: [
  170. /*CircleAvatar(
  171. backgroundImage: NetworkImage(post.author),
  172. radius: 20,
  173. ),*/
  174. const SizedBox(width: 12),
  175. Expanded(
  176. child: Column(
  177. crossAxisAlignment: CrossAxisAlignment.start,
  178. children: [
  179. Text(
  180. post.title,
  181. style: Theme.of(context).textTheme.titleMedium?.copyWith(
  182. fontWeight: FontWeight.w600,
  183. ),
  184. ),
  185. ],
  186. ),
  187. ),
  188. // Time and menu
  189. Text(
  190. post.published,
  191. style: Theme.of(context).textTheme.bodySmall,
  192. ),
  193. /*IconButton(
  194. icon: const Icon(Icons.more_vert),
  195. onPressed: () => _showPostMenu(context),
  196. ),*/
  197. ],
  198. ),
  199. );
  200. }
  201. }
  202. // New footer component
  203. class _PostActionsFooter extends StatelessWidget {
  204. final Post post;
  205. const _PostActionsFooter({required this.post});
  206. @override
  207. Widget build(BuildContext context) {
  208. return Padding(
  209. padding: const EdgeInsets.only(top: 12),
  210. child: Row(
  211. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  212. children: [
  213. // Interactive buttons
  214. Row(
  215. children: [/*
  216. IconButton(
  217. icon: Icon(
  218. post.isLiked ? Icons.favorite : Icons.favorite_border,
  219. color: post.isLiked ? Colors.red : null,
  220. ),
  221. onPressed: () => _toggleLike(),
  222. ),
  223. Text(_formatCount(post.likeCount)),
  224. const SizedBox(width: 16),
  225. IconButton(
  226. icon: const Icon(Icons.comment_outlined),
  227. onPressed: () => _showComments(),
  228. ),
  229. Text(_formatCount(post.commentCount)),
  230. */],
  231. ),
  232. // Share button
  233. IconButton(
  234. icon: const Icon(Icons.share_outlined),
  235. onPressed: () => _sharePost(post),
  236. ),
  237. ],
  238. ),
  239. );
  240. }
  241. }
  242. void _sharePost(Post post) {
  243. var link = 'https://kemono.su/${post.service}/user/${post.user}/post/${post.id}';
  244. Share.share(link);
  245. }
  246. Future<bool> _handleUrlLaunch(BuildContext context, String url) async {
  247. try {
  248. await launchUrl(
  249. Uri.parse(url),
  250. mode: LaunchMode.externalApplication,
  251. );
  252. } catch (e) {
  253. ScaffoldMessenger.of(context).showSnackBar(
  254. SnackBar(content: Text('Could not open link: ${e.toString()}')),
  255. );
  256. }
  257. return true;
  258. }