post_detail_view.dart 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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/models/post.dart';
  6. import 'package:veloe_kemono_party_flutter/pages/widgets/smart_image_container.dart';
  7. class PostDetailView extends StatelessWidget {
  8. final Post post;
  9. const PostDetailView({super.key, required this.post});
  10. @override
  11. Widget build(BuildContext context) {
  12. return Scaffold(
  13. appBar: AppBar(
  14. title: Text(post.title),
  15. elevation: 0,
  16. ),
  17. body: LayoutBuilder(
  18. builder: (context, constraints) {
  19. return ConstrainedBox(
  20. constraints: BoxConstraints(
  21. minHeight: constraints.maxHeight,
  22. maxWidth: constraints.maxWidth,
  23. ),
  24. child: Column(
  25. mainAxisSize: MainAxisSize.min,
  26. crossAxisAlignment: CrossAxisAlignment.start,
  27. children: [
  28. // Header Section
  29. _buildTitle(context),
  30. // Content Area
  31. Flexible(
  32. child: Column(
  33. mainAxisSize: MainAxisSize.min,
  34. children: [
  35. if (post.attachments.isNotEmpty)
  36. _buildImageColumn(post.attachments),
  37. _buildHtmlContent(),
  38. ],
  39. ),
  40. ),
  41. // Footer Section
  42. _buildDateDisplay(context),
  43. ],
  44. ),
  45. );
  46. },
  47. ),
  48. );
  49. }
  50. Widget _buildTitle(BuildContext context) {
  51. return Padding(
  52. padding: const EdgeInsets.all(16.0),
  53. child: Text(
  54. post.title,
  55. style: Theme.of(context).textTheme.headlineMedium?.copyWith(
  56. fontWeight: FontWeight.w600,
  57. ),
  58. ),
  59. );
  60. }
  61. Widget _buildImageColumn(List<Attachment> attachments) {
  62. return Column(
  63. children: attachments
  64. .where((attachment) => _isImageFile(attachment.link))
  65. .map((attachment) {
  66. return Padding(
  67. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  68. child: SmartImageContainer(imageUrl: attachment.link),
  69. );
  70. }).toList(),
  71. );
  72. }
  73. bool _isImageFile(String url) {
  74. try {
  75. final uri = Uri.parse(url);
  76. final path = uri.path.toLowerCase();
  77. final cleanPath = path.split('?').first.split('#').first;
  78. const imageExtensions = {
  79. 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg',
  80. 'tiff', 'heic', 'heif'
  81. };
  82. return imageExtensions.any((ext) => cleanPath.endsWith('.$ext'));
  83. } catch (e) {
  84. return false; // Invalid URL format
  85. }
  86. }
  87. Widget _buildHtmlContent() {
  88. return Padding(
  89. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
  90. child: HtmlWidget(
  91. post.content,
  92. renderMode: RenderMode.column,
  93. onTapUrl: (url) => launchUrl(Uri.parse(url)),
  94. customWidgetBuilder: (element) {
  95. if (element.localName == 'img') {
  96. final src = element.attributes['src'];
  97. return src != null
  98. ? Padding(
  99. padding: const EdgeInsets.symmetric(vertical: 8),
  100. child: SmartImageContainer(imageUrl: src),
  101. )
  102. : const SizedBox.shrink();
  103. }
  104. return null;
  105. },
  106. ),
  107. );
  108. }
  109. Widget _buildDateDisplay(BuildContext context) {
  110. return Padding(
  111. padding: const EdgeInsets.all(16.0),
  112. child: Row(
  113. children: [
  114. Icon(Icons.calendar_today, size: 18, color: Colors.grey[700]),
  115. const SizedBox(width: 8),
  116. Text(post.published
  117. ,
  118. style: Theme.of(context).textTheme.bodyLarge?.copyWith(
  119. color: Colors.grey[700],
  120. ),
  121. ),
  122. ],
  123. ),
  124. );
  125. }
  126. }