post_container.dart 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 'smart_image_container.dart';
  6. import 'package:share_plus/share_plus.dart';
  7. import '../../models/post.dart';
  8. class PostContainer extends StatelessWidget {
  9. final Post post;
  10. const PostContainer({super.key, required this.post});
  11. @override
  12. Widget build(BuildContext context) {
  13. return Card(
  14. margin: const EdgeInsets.all(8),
  15. child: Padding(
  16. padding: const EdgeInsets.all(16),
  17. child: Column(
  18. mainAxisSize: MainAxisSize.min,
  19. crossAxisAlignment: CrossAxisAlignment.start,
  20. children: [
  21. _PostHeader(post: post),
  22. _PostContentSection(post: post),
  23. _PostActionsFooter(post: post),
  24. ],
  25. ),
  26. ),
  27. );
  28. }
  29. }
  30. class _PostHeader extends StatelessWidget {
  31. final Post post;
  32. const _PostHeader({required this.post});
  33. @override
  34. Widget build(BuildContext context) {
  35. return Padding(
  36. padding: const EdgeInsets.only(bottom: 12),
  37. child: Row(
  38. children: [
  39. /*CircleAvatar(
  40. backgroundImage: NetworkImage(post.author),
  41. radius: 20,
  42. ),*/
  43. const SizedBox(width: 12),
  44. Expanded(
  45. child: Column(
  46. crossAxisAlignment: CrossAxisAlignment.start,
  47. children: [
  48. Text(
  49. post.title,
  50. style: Theme.of(context).textTheme.titleMedium?.copyWith(
  51. fontWeight: FontWeight.w600,
  52. ),
  53. ),
  54. ],
  55. ),
  56. ),
  57. // Time and menu
  58. Text(
  59. post.published,
  60. style: Theme.of(context).textTheme.bodySmall,
  61. ),
  62. /*IconButton(
  63. icon: const Icon(Icons.more_vert),
  64. onPressed: () => _showPostMenu(context),
  65. ),*/
  66. ],
  67. ),
  68. );
  69. }
  70. }
  71. // New footer component
  72. class _PostActionsFooter extends StatelessWidget {
  73. final Post post;
  74. const _PostActionsFooter({required this.post});
  75. @override
  76. Widget build(BuildContext context) {
  77. return Padding(
  78. padding: const EdgeInsets.only(top: 12),
  79. child: Row(
  80. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  81. children: [
  82. // Interactive buttons
  83. Row(
  84. children: [/*
  85. IconButton(
  86. icon: Icon(
  87. post.isLiked ? Icons.favorite : Icons.favorite_border,
  88. color: post.isLiked ? Colors.red : null,
  89. ),
  90. onPressed: () => _toggleLike(),
  91. ),
  92. Text(_formatCount(post.likeCount)),
  93. const SizedBox(width: 16),
  94. IconButton(
  95. icon: const Icon(Icons.comment_outlined),
  96. onPressed: () => _showComments(),
  97. ),
  98. Text(_formatCount(post.commentCount)),
  99. */],
  100. ),
  101. // Share button
  102. IconButton(
  103. icon: const Icon(Icons.share_outlined),
  104. onPressed: () => _sharePost(post),
  105. ),
  106. ],
  107. ),
  108. );
  109. }
  110. }
  111. void _sharePost(Post post) {
  112. var link = 'https://kemono.su/${post.service}/user/${post.user}/post/${post.id}';
  113. Share.share(link);
  114. }
  115. class _PostContentSection extends StatelessWidget {
  116. final Post post;
  117. const _PostContentSection({required this.post});
  118. @override
  119. Widget build(BuildContext context) {
  120. final imageAttachments = post.attachments
  121. .where((a) => _isImageFile(a.link))
  122. .toList();
  123. return Column(
  124. mainAxisSize: MainAxisSize.min,
  125. crossAxisAlignment: CrossAxisAlignment.start,
  126. children: [
  127. // Text Content
  128. HtmlWidget(
  129. post.content,
  130. textStyle: Theme.of(context).textTheme.bodyMedium,
  131. onTapUrl: (url) => _handleUrlLaunch(context, url),
  132. ),
  133. // Attachments Grid
  134. if (imageAttachments.isNotEmpty)
  135. LayoutBuilder(
  136. builder: (context, constraints) {
  137. return Column(
  138. children: [
  139. const SizedBox(height: 12),
  140. GridView.builder(
  141. shrinkWrap: true,
  142. physics: const NeverScrollableScrollPhysics(),
  143. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  144. crossAxisCount: _calculateColumnCount(constraints.maxWidth),
  145. ),
  146. itemCount: imageAttachments.length,
  147. itemBuilder: (context, index) => Padding(
  148. padding: const EdgeInsets.all(4),
  149. child: SmartImageContainer(
  150. imageUrl: imageAttachments[index].link,
  151. //onTap: () => _handleAttachmentTap(index),
  152. ),
  153. ),
  154. ),
  155. // Attachment links list
  156. ...post.attachments.map(
  157. (attachment) => _buildAttachmentLink(context,attachment),
  158. ),
  159. ],
  160. );
  161. },
  162. ),
  163. ],
  164. );
  165. }
  166. Widget _buildAttachmentLink(BuildContext context,Attachment attachment) {
  167. return Padding(
  168. padding: const EdgeInsets.only(top: 8),
  169. child: InkWell(
  170. onTap: () => _handleUrlLaunch(context, attachment.link),
  171. child: Row(
  172. children: [
  173. const Icon(Icons.link, size: 16),
  174. const SizedBox(width: 8),
  175. Expanded(
  176. child: Text(
  177. attachment.name ?? _parseFileName(attachment.link),
  178. style: const TextStyle(
  179. color: Colors.blue,
  180. decoration: TextDecoration.underline,
  181. ),
  182. overflow: TextOverflow.ellipsis,
  183. ),
  184. ),
  185. ],
  186. ),
  187. ),
  188. );
  189. }
  190. int _calculateColumnCount(double availableWidth) {
  191. return (availableWidth / 400).floor().clamp(1, 3);
  192. }
  193. String _parseFileName(String url) {
  194. try {
  195. return Uri.parse(url).pathSegments.last;
  196. } catch (e) {
  197. return 'Download';
  198. }
  199. }
  200. bool _isImageFile(String url) {
  201. try {
  202. final uri = Uri.parse(url);
  203. final path = uri.path.toLowerCase();
  204. final cleanPath = path.split('?').first.split('#').first;
  205. const imageExtensions = {
  206. 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg',
  207. 'tiff', 'heic', 'heif'
  208. };
  209. return imageExtensions.any((ext) => cleanPath.endsWith('.$ext'));
  210. } catch (e) {
  211. return false; // Invalid URL format
  212. }
  213. }
  214. }
  215. Future<bool> _handleUrlLaunch(BuildContext context, String url) async {
  216. try {
  217. await launchUrl(
  218. Uri.parse(url),
  219. mode: LaunchMode.externalApplication,
  220. );
  221. } catch (e) {
  222. ScaffoldMessenger.of(context).showSnackBar(
  223. SnackBar(content: Text('Could not open link: ${e.toString()}')),
  224. );
  225. }
  226. return true;
  227. }