Compare commits
	
		
			1 Commits
		
	
	
		
			3.2.0+124
			...
			8236d31ecc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8236d31ecc | 
| @@ -573,6 +573,7 @@ | ||||
|   "keyboardShortcuts": "Keyboard Shortcuts", | ||||
|   "share": "Share", | ||||
|   "sharePost": "Share Post", | ||||
|   "sharePostPhoto": "Share Post as Photo", | ||||
|   "quickActions": "Quick Actions", | ||||
|   "post": "Post", | ||||
|   "copy": "Copy", | ||||
|   | ||||
| @@ -572,6 +572,7 @@ | ||||
|   "postContentEmpty": "发布的内容不能为空", | ||||
|   "share": "分享", | ||||
|   "sharePost": "分享帖子", | ||||
|   "sharePostAsPhoto": "通过图片分享帖子", | ||||
|   "quickActions": "快捷操作", | ||||
|   "post": "帖子", | ||||
|   "copy": "复制", | ||||
|   | ||||
| @@ -14,6 +14,7 @@ class PollSubmit extends ConsumerStatefulWidget { | ||||
|     this.initialAnswers, | ||||
|     this.onCancel, | ||||
|     this.showProgress = true, | ||||
|     this.isReadonly = false, | ||||
|   }); | ||||
|  | ||||
|   final SnPollWithStats poll; | ||||
| @@ -31,6 +32,8 @@ class PollSubmit extends ConsumerStatefulWidget { | ||||
|   /// Whether to show a progress indicator (e.g., "2 / N"). | ||||
|   final bool showProgress; | ||||
|  | ||||
|   final bool isReadonly; | ||||
|  | ||||
|   @override | ||||
|   ConsumerState<PollSubmit> createState() => _PollSubmitState(); | ||||
| } | ||||
| @@ -59,7 +62,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|     _questions = [...widget.poll.questions] | ||||
|       ..sort((a, b) => a.order.compareTo(b.order)); | ||||
|     _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); | ||||
|     _loadCurrentIntoLocalState(); | ||||
|     if (!widget.isReadonly) { | ||||
|       _loadCurrentIntoLocalState(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -74,7 +79,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|           [...widget.poll.questions] | ||||
|             ..sort((a, b) => a.order.compareTo(b.order)), | ||||
|         ); | ||||
|       _loadCurrentIntoLocalState(); | ||||
|       if (!widget.isReadonly) { | ||||
|         _loadCurrentIntoLocalState(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -259,10 +266,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|                     child: Text( | ||||
|                       widget.poll.description!, | ||||
|                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                         color: Theme.of( | ||||
|                           context, | ||||
|                         ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||
|                       ), | ||||
|                             color: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||
|                           ), | ||||
|                     ), | ||||
|                   ), | ||||
|               ], | ||||
| @@ -287,8 +294,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|                 child: Text( | ||||
|                   '*', | ||||
|                   style: Theme.of(context).textTheme.titleMedium?.copyWith( | ||||
|                     color: Theme.of(context).colorScheme.error, | ||||
|                   ), | ||||
|                         color: Theme.of(context).colorScheme.error, | ||||
|                       ), | ||||
|                 ), | ||||
|               ), | ||||
|           ], | ||||
| @@ -299,10 +306,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|             child: Text( | ||||
|               q.description!, | ||||
|               style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||
|                 color: Theme.of( | ||||
|                   context, | ||||
|                 ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||
|               ), | ||||
|                     color: Theme.of( | ||||
|                       context, | ||||
|                     ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
| @@ -340,14 +347,12 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|       case SnPollQuestionType.yesNo: | ||||
|         // yes/no: map {true: count, false: count} | ||||
|         if (raw is Map) { | ||||
|           final int yes = | ||||
|               (raw[true] is int) | ||||
|                   ? raw[true] as int | ||||
|                   : int.tryParse('${raw[true]}') ?? 0; | ||||
|           final int no = | ||||
|               (raw[false] is int) | ||||
|                   ? raw[false] as int | ||||
|                   : int.tryParse('${raw[false]}') ?? 0; | ||||
|           final int yes = (raw[true] is int) | ||||
|               ? raw[true] as int | ||||
|               : int.tryParse('${raw[true]}') ?? 0; | ||||
|           final int no = (raw[false] is int) | ||||
|               ? raw[false] as int | ||||
|               : int.tryParse('${raw[false]}') ?? 0; | ||||
|           final total = (yes + no).clamp(0, 1 << 31); | ||||
|           final yesPct = total == 0 ? 0.0 : yes / total; | ||||
|           final noPct = total == 0 ? 0.0 : no / total; | ||||
| @@ -415,8 +420,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|                 Text( | ||||
|                   'Total: $total', | ||||
|                   style: Theme.of(context).textTheme.labelSmall?.copyWith( | ||||
|                     color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                   ), | ||||
|                         color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                       ), | ||||
|                 ), | ||||
|             ], | ||||
|           ); | ||||
| @@ -445,8 +450,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|               Text( | ||||
|                 'Stats', | ||||
|                 style: Theme.of(context).textTheme.labelLarge?.copyWith( | ||||
|                   color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                 ), | ||||
|                       color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                     ), | ||||
|               ), | ||||
|               const SizedBox(height: 8), | ||||
|               body, | ||||
| @@ -577,14 +582,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|         ), | ||||
|         const Spacer(), | ||||
|         FilledButton.icon( | ||||
|           icon: | ||||
|               _submitting | ||||
|                   ? const SizedBox( | ||||
|                     width: 16, | ||||
|                     height: 16, | ||||
|                     child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                   ) | ||||
|                   : Icon(isLast ? Icons.check : Icons.arrow_forward), | ||||
|           icon: _submitting | ||||
|               ? const SizedBox( | ||||
|                   width: 16, | ||||
|                   height: 16, | ||||
|                   child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                 ) | ||||
|               : Icon(isLast ? Icons.check : Icons.arrow_forward), | ||||
|           label: Text(isLast ? 'Submit' : 'Next'), | ||||
|           onPressed: canProceed ? _next : null, | ||||
|         ), | ||||
| @@ -592,12 +596,92 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildReadonlyView(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         if (widget.poll.title != null || widget.poll.description != null) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(bottom: 12), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 if (widget.poll.title != null) | ||||
|                   Text( | ||||
|                     widget.poll.title!, | ||||
|                     style: Theme.of(context).textTheme.titleLarge, | ||||
|                   ), | ||||
|                 if (widget.poll.description != null) | ||||
|                   Padding( | ||||
|                     padding: const EdgeInsets.only(top: 4), | ||||
|                     child: Text( | ||||
|                       widget.poll.description!, | ||||
|                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                             color: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||
|                           ), | ||||
|                     ), | ||||
|                   ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         for (final q in _questions) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(bottom: 16.0), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     Expanded( | ||||
|                       child: Text( | ||||
|                         q.title, | ||||
|                         style: Theme.of(context).textTheme.titleMedium, | ||||
|                       ), | ||||
|                     ), | ||||
|                     if (q.isRequired) | ||||
|                       Padding( | ||||
|                         padding: const EdgeInsets.only(left: 8), | ||||
|                         child: Text( | ||||
|                           '*', | ||||
|                           style: Theme.of(context).textTheme.titleMedium?.copyWith( | ||||
|                                 color: Theme.of(context).colorScheme.error, | ||||
|                               ), | ||||
|                         ), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 if (q.description != null) | ||||
|                   Padding( | ||||
|                     padding: const EdgeInsets.only(top: 4), | ||||
|                     child: Text( | ||||
|                       q.description!, | ||||
|                       style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||
|                             color: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||
|                           ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 _buildStats(context, q), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (_questions.isEmpty) { | ||||
|       return const SizedBox.shrink(); | ||||
|     } | ||||
|  | ||||
|     if (widget.isReadonly) { | ||||
|       return _buildReadonlyView(context); | ||||
|     } | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
| @@ -647,10 +731,9 @@ class _BarStatRow extends StatelessWidget { | ||||
|     final bgColor = Theme.of( | ||||
|       context, | ||||
|     ).colorScheme.surfaceVariant.withOpacity(0.6); | ||||
|     final fg = | ||||
|         (fraction.isNaN || fraction.isInfinite) | ||||
|             ? 0.0 | ||||
|             : fraction.clamp(0.0, 1.0); | ||||
|     final fg = (fraction.isNaN || fraction.isInfinite) | ||||
|         ? 0.0 | ||||
|         : fraction.clamp(0.0, 1.0); | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -7,11 +7,9 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||
| import 'package:island/widgets/content/markdown.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/post_shared.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:super_context_menu/super_context_menu.dart'; | ||||
|  | ||||
| class PostItemCreator extends HookConsumerWidget { | ||||
| @@ -81,7 +79,6 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|               title: 'copyLink'.tr(), | ||||
|               image: MenuImage.icon(Symbols.link), | ||||
|               callback: () { | ||||
|                 // Copy post link to clipboard | ||||
|                 context.pushNamed( | ||||
|                   'postDetail', | ||||
|                   pathParameters: {'id': item.id}, | ||||
| @@ -105,8 +102,9 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 _buildPostHeader(context), | ||||
|                 _buildPostContent(context), | ||||
|                 PostHeader(item: item), | ||||
|                 PostBody(item: item), | ||||
|                 ReferencedPostWidget(item: item), | ||||
|                 const Gap(16), | ||||
|                 _buildAnalyticsSection(context), | ||||
|               ], | ||||
| @@ -117,128 +115,12 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildPostHeader(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         // Post ID and timestamp row | ||||
|         Row( | ||||
|           children: [ | ||||
|             Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|               decoration: BoxDecoration( | ||||
|                 color: Theme.of(context).colorScheme.primaryContainer, | ||||
|                 borderRadius: BorderRadius.circular(4), | ||||
|               ), | ||||
|               child: Text( | ||||
|                 'ID: ${item.id.substring(0, 6)}', | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 12, | ||||
|                   fontWeight: FontWeight.bold, | ||||
|                   color: Theme.of(context).colorScheme.onPrimaryContainer, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             Icon( | ||||
|               _getVisibilityIcon(item.visibility), | ||||
|               size: 16, | ||||
|               color: Theme.of(context).colorScheme.secondary, | ||||
|             ), | ||||
|             const SizedBox(width: 4), | ||||
|             Text( | ||||
|               _getVisibilityText(item.visibility).tr(), | ||||
|               style: TextStyle( | ||||
|                 fontSize: 12, | ||||
|                 color: Theme.of(context).colorScheme.secondary, | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               item.publishedAt?.formatSystem() ?? '', | ||||
|               style: TextStyle( | ||||
|                 fontSize: 12, | ||||
|                 color: Theme.of(context).colorScheme.secondary, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         const Gap(8), | ||||
|  | ||||
|         // Title and description | ||||
|         if (item.title?.isNotEmpty ?? false) | ||||
|           Text( | ||||
|             item.title!, | ||||
|             style: Theme.of( | ||||
|               context, | ||||
|             ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), | ||||
|           ), | ||||
|         if (item.description?.isNotEmpty ?? false) | ||||
|           Text( | ||||
|             item.description!, | ||||
|             style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|               color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|             ), | ||||
|           ).padding(top: 4), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildPostContent(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         // Content preview | ||||
|         if (item.content?.isNotEmpty ?? false) | ||||
|           Container( | ||||
|             margin: const EdgeInsets.only(top: 12), | ||||
|             child: MarkdownTextContent(content: item.content!), | ||||
|           ), | ||||
|  | ||||
|         // Attachments | ||||
|         if (item.attachments.isNotEmpty) | ||||
|           CloudFileList( | ||||
|             files: item.attachments, | ||||
|             maxWidth: MediaQuery.of(context).size.width * 0.85, | ||||
|             padding: EdgeInsets.only(top: 8), | ||||
|           ), | ||||
|  | ||||
|         // Reference post indicator | ||||
|         if (item.repliedPost != null || item.forwardedPost != null) | ||||
|           Container( | ||||
|             margin: const EdgeInsets.only(top: 8), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 Icon( | ||||
|                   item.repliedPost != null ? Symbols.reply : Symbols.forward, | ||||
|                   size: 16, | ||||
|                   color: Theme.of(context).colorScheme.secondary, | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 Text( | ||||
|                   item.repliedPost != null | ||||
|                       ? 'repliedTo'.tr() | ||||
|                       : 'forwarded'.tr(), | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 12, | ||||
|                     color: Theme.of(context).colorScheme.secondary, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildAnalyticsSection(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Text('Analytics', style: Theme.of(context).textTheme.titleSmall), | ||||
|         const Gap(8), | ||||
|  | ||||
|         // Engagement metrics in a card | ||||
|         Card( | ||||
|           elevation: 1, | ||||
|           margin: EdgeInsets.zero, | ||||
| @@ -279,15 +161,9 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|           ), | ||||
|         ), | ||||
|         const Gap(16), | ||||
|  | ||||
|         // Reactions summary | ||||
|         if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), | ||||
|  | ||||
|         // Metadata section | ||||
|         if (item.meta != null && item.meta!.isNotEmpty) | ||||
|           _buildMetadataSection(context), | ||||
|  | ||||
|         // Creation and modification timestamps | ||||
|         const Gap(16), | ||||
|         Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
| @@ -425,31 +301,3 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Helper method to get the appropriate icon for each visibility status | ||||
| IconData _getVisibilityIcon(int visibility) { | ||||
|   switch (visibility) { | ||||
|     case 1: // Friends | ||||
|       return Symbols.group; | ||||
|     case 2: // Unlisted | ||||
|       return Symbols.link_off; | ||||
|     case 3: // Private | ||||
|       return Symbols.lock; | ||||
|     default: // Public (0) or unknown | ||||
|       return Symbols.public; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Helper method to get the translation key for each visibility status | ||||
| String _getVisibilityText(int visibility) { | ||||
|   switch (visibility) { | ||||
|     case 1: // Friends | ||||
|       return 'postVisibilityFriends'; | ||||
|     case 2: // Unlisted | ||||
|       return 'postVisibilityUnlisted'; | ||||
|     case 3: // Private | ||||
|       return 'postVisibilityPrivate'; | ||||
|     default: // Public (0) or unknown | ||||
|       return 'postVisibilityPublic'; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										72
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/widgets/post/post_shared.dart'; | ||||
|  | ||||
| class PostItemScreenshot extends ConsumerWidget { | ||||
|   final SnPost item; | ||||
|   final EdgeInsets? padding; | ||||
|   final bool isFullPost; | ||||
|   final bool isShowReference; | ||||
|   const PostItemScreenshot({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.padding, | ||||
|     this.isFullPost = false, | ||||
|     this.isShowReference = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final renderingPadding = | ||||
|         padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); | ||||
|  | ||||
|     final mostReaction = | ||||
|         item.reactionsCount.isEmpty | ||||
|             ? null | ||||
|             : item.reactionsCount.entries | ||||
|                 .sortedBy((e) => e.value) | ||||
|                 .map((e) => e.key) | ||||
|                 .last; | ||||
|  | ||||
|     return Column( | ||||
|       mainAxisSize: MainAxisSize.min, | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         PostHeader( | ||||
|           item: item, | ||||
|           isFullPost: isFullPost, | ||||
|           isInteractive: false, | ||||
|           renderingPadding: renderingPadding, | ||||
|           trailing: | ||||
|               mostReaction != null | ||||
|                   ? Row( | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         kReactionTemplates[mostReaction]?.icon ?? '', | ||||
|                         style: const TextStyle(fontSize: 20), | ||||
|                       ), | ||||
|                       const Gap(4), | ||||
|                       Text( | ||||
|                         'x${item.reactionsCount[mostReaction]}', | ||||
|                         style: const TextStyle(fontSize: 11), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ) | ||||
|                   : null, | ||||
|         ), | ||||
|         PostBody( | ||||
|           item: item, | ||||
|           renderingPadding: renderingPadding, | ||||
|           isFullPost: isFullPost, | ||||
|           isTextSelectable: false, | ||||
|           isInteractive: false, | ||||
|         ), | ||||
|         if (isShowReference) | ||||
|           ReferencedPostWidget(item: item, isInteractive: false), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										818
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										818
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,818 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/embed.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/utils/mapping.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/embed/link.dart'; | ||||
| import 'package:island/widgets/content/markdown.dart'; | ||||
| import 'package:island/widgets/poll/poll_submit.dart'; | ||||
| import 'package:island/widgets/post/post_replies_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'post_shared.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<SnPost?> postFeaturedReply(Ref ref, String id) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await client.get('/sphere/posts/$id/replies/featured'); | ||||
|     return SnPost.fromJson(resp.data); | ||||
|   } catch (_) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostVisibilityHelpers { | ||||
|   static IconData getVisibilityIcon(int visibility) { | ||||
|     switch (visibility) { | ||||
|       case 1: | ||||
|         return Symbols.group; | ||||
|       case 2: | ||||
|         return Symbols.link_off; | ||||
|       case 3: | ||||
|         return Symbols.lock; | ||||
|       default: | ||||
|         return Symbols.public; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static String getVisibilityText(int visibility) { | ||||
|     switch (visibility) { | ||||
|       case 1: | ||||
|         return 'postVisibilityFriends'; | ||||
|       case 2: | ||||
|         return 'postVisibilityUnlisted'; | ||||
|       case 3: | ||||
|         return 'postVisibilityPrivate'; | ||||
|       default: | ||||
|         return 'postVisibilityPublic'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostReplyPreview extends HookConsumerWidget { | ||||
|   final SnPost parent; | ||||
|   final bool isOpenable; | ||||
|   final bool isCompact; | ||||
|   final bool isAutoload; | ||||
|   final VoidCallback? onOpen; | ||||
|   const PostReplyPreview({ | ||||
|     super.key, | ||||
|     required this.parent, | ||||
|     this.isOpenable = false, | ||||
|     this.isCompact = false, | ||||
|     this.isAutoload = true, | ||||
|     this.onOpen, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final posts = useState<List<SnPost>>([]); | ||||
|     final loading = useState(false); | ||||
|  | ||||
|     Future<void> fetchMoreReplies({int pageSize = 3}) async { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       loading.value = true; | ||||
|  | ||||
|       try { | ||||
|         final response = await client.get( | ||||
|           '/sphere/posts/${parent.id}/replies', | ||||
|           queryParameters: {'offset': posts.value.length, 'take': pageSize}, | ||||
|         ); | ||||
|         try { | ||||
|           posts.value = [ | ||||
|             ...posts.value, | ||||
|             ...response.data.map((e) => SnPost.fromJson(e)), | ||||
|           ]; | ||||
|         } catch (_) { | ||||
|           // ignore disposed | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         try { | ||||
|           loading.value = false; | ||||
|         } catch (_) { | ||||
|           // ignore disposed | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (isAutoload) fetchMoreReplies(); | ||||
|       return null; | ||||
|     }, [parent]); | ||||
|  | ||||
|     final featuredReply = | ||||
|         isOpenable ? null : ref.watch(PostFeaturedReplyProvider(parent.id)); | ||||
|  | ||||
|     final itemWidget = | ||||
|         isOpenable | ||||
|             ? Column( | ||||
|               children: [ | ||||
|                 for (final post in posts.value) | ||||
|                   Column( | ||||
|                     children: [ | ||||
|                       InkWell( | ||||
|                         child: Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           spacing: 8, | ||||
|                           children: [ | ||||
|                             ProfilePictureWidget( | ||||
|                               file: post.publisher.picture, | ||||
|                               radius: 12, | ||||
|                             ).padding(top: 4), | ||||
|                             if (post.content?.isNotEmpty ?? false) | ||||
|                               Expanded( | ||||
|                                 child: MarkdownTextContent( | ||||
|                                   content: post.content!, | ||||
|                                 ).padding(top: 2), | ||||
|                               ) | ||||
|                             else | ||||
|                               Expanded( | ||||
|                                 child: Text( | ||||
|                                   'postHasAttachments', | ||||
|                                 ).plural(post.attachments.length), | ||||
|                               ), | ||||
|                           ], | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           onOpen?.call(); | ||||
|                           context.pushNamed( | ||||
|                             'postDetail', | ||||
|                             pathParameters: {'id': post.id}, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                       if (post.repliesCount > 0) | ||||
|                         PostReplyPreview( | ||||
|                           parent: post, | ||||
|                           isOpenable: true, | ||||
|                           isCompact: true, | ||||
|                           isAutoload: false, | ||||
|                           onOpen: onOpen, | ||||
|                         ).padding(left: 24), | ||||
|                     ], | ||||
|                   ), | ||||
|                 if (loading.value) | ||||
|                   Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       SizedBox( | ||||
|                         width: 16, | ||||
|                         height: 16, | ||||
|                         child: CircularProgressIndicator(), | ||||
|                       ), | ||||
|                       Text('loading').tr(), | ||||
|                     ], | ||||
|                   ) | ||||
|                 else if (posts.value.length < parent.repliesCount) | ||||
|                   InkWell( | ||||
|                     child: Row( | ||||
|                       spacing: 8, | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.keyboard_arrow_down, size: 20), | ||||
|                         Text('repliesLoadMore').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       fetchMoreReplies(); | ||||
|                     }, | ||||
|                   ), | ||||
|               ], | ||||
|             ) | ||||
|             : (featuredReply!).map( | ||||
|               data: | ||||
|                   (data) => Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       ProfilePictureWidget( | ||||
|                         file: data.value?.publisher.picture, | ||||
|                         radius: 12, | ||||
|                       ).padding(top: 4), | ||||
|                       if (data.value?.content?.isNotEmpty ?? false) | ||||
|                         Expanded( | ||||
|                           child: MarkdownTextContent( | ||||
|                             content: data.value!.content!, | ||||
|                           ), | ||||
|                         ) | ||||
|                       else | ||||
|                         Expanded( | ||||
|                           child: Text( | ||||
|                             'postHasAttachments', | ||||
|                           ).plural(data.value?.attachments.length ?? 0), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ), | ||||
|               error: | ||||
|                   (e) => Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.close, size: 18), | ||||
|                       Text(e.error.toString()), | ||||
|                     ], | ||||
|                   ), | ||||
|               loading: | ||||
|                   (_) => Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       SizedBox( | ||||
|                         width: 16, | ||||
|                         height: 16, | ||||
|                         child: CircularProgressIndicator(), | ||||
|                       ), | ||||
|                       Text('loading').tr(), | ||||
|                     ], | ||||
|                   ), | ||||
|             ); | ||||
|  | ||||
|     final contentWidget = | ||||
|         isCompact | ||||
|             ? itemWidget | ||||
|             : Container( | ||||
|               padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|               decoration: BoxDecoration( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                 border: Border.all( | ||||
|                   color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||
|                 ), | ||||
|                 borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|               ), | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   Text('repliesCount') | ||||
|                       .plural(parent.repliesCount) | ||||
|                       .fontSize(15) | ||||
|                       .bold() | ||||
|                       .padding(horizontal: 5), | ||||
|                   itemWidget, | ||||
|                 ], | ||||
|               ), | ||||
|             ); | ||||
|  | ||||
|     return InkWell( | ||||
|       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|       onTap: () { | ||||
|         showModalBottomSheet( | ||||
|           context: context, | ||||
|           isScrollControlled: true, | ||||
|           useRootNavigator: true, | ||||
|           builder: (context) => PostRepliesSheet(post: parent), | ||||
|         ); | ||||
|       }, | ||||
|       child: contentWidget, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostTruncateHint extends StatelessWidget { | ||||
|   final bool isCompact; | ||||
|   final EdgeInsets? margin; | ||||
|   final bool withArrow; | ||||
|  | ||||
|   const PostTruncateHint({ | ||||
|     super.key, | ||||
|     this.isCompact = false, | ||||
|     this.margin, | ||||
|     this.withArrow = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       margin: margin ?? EdgeInsets.only(top: isCompact ? 4 : 8), | ||||
|       padding: EdgeInsets.symmetric( | ||||
|         horizontal: isCompact ? 8 : 12, | ||||
|         vertical: isCompact ? 4 : 8, | ||||
|       ), | ||||
|       decoration: BoxDecoration( | ||||
|         color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), | ||||
|         borderRadius: BorderRadius.circular(8), | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).colorScheme.outline.withOpacity(0.2), | ||||
|         ), | ||||
|       ), | ||||
|       child: Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           Icon( | ||||
|             Symbols.more_horiz, | ||||
|             size: isCompact ? 14 : 16, | ||||
|             color: Theme.of(context).colorScheme.secondary, | ||||
|           ), | ||||
|           SizedBox(width: isCompact ? 4 : 6), | ||||
|           Flexible( | ||||
|             child: Text( | ||||
|               'postTruncated'.tr(), | ||||
|               style: TextStyle( | ||||
|                 fontSize: isCompact ? 10 : 12, | ||||
|                 color: Theme.of(context).colorScheme.secondary, | ||||
|                 fontStyle: FontStyle.italic, | ||||
|               ), | ||||
|               maxLines: 1, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|             ), | ||||
|           ), | ||||
|           if (withArrow) ...[ | ||||
|             SizedBox(width: isCompact ? 3 : 4), | ||||
|             Icon( | ||||
|               Symbols.arrow_forward, | ||||
|               size: isCompact ? 12 : 14, | ||||
|               color: Theme.of(context).colorScheme.secondary, | ||||
|             ), | ||||
|           ], | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ReferencedPostWidget extends StatelessWidget { | ||||
|   final SnPost item; | ||||
|   final bool isInteractive; | ||||
|  | ||||
|   const ReferencedPostWidget({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.isInteractive = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final referencePost = item.repliedPost ?? item.forwardedPost; | ||||
|     if (referencePost == null) return const SizedBox.shrink(); | ||||
|  | ||||
|     final isReply = item.repliedPost != null; | ||||
|  | ||||
|     final content = Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||
|       margin: const EdgeInsets.only(top: 8), | ||||
|       decoration: BoxDecoration( | ||||
|         color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), | ||||
|         borderRadius: BorderRadius.circular(12), | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||
|         ), | ||||
|       ), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             children: [ | ||||
|               Icon( | ||||
|                 isReply ? Symbols.reply : Symbols.forward, | ||||
|                 size: 16, | ||||
|                 color: Theme.of(context).colorScheme.secondary, | ||||
|               ), | ||||
|               const SizedBox(width: 6), | ||||
|               Text( | ||||
|                 isReply ? 'repliedTo'.tr() : 'forwarded'.tr(), | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).colorScheme.secondary, | ||||
|                   fontWeight: FontWeight.w500, | ||||
|                   fontSize: 12, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           const SizedBox(height: 8), | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               ProfilePictureWidget( | ||||
|                 fileId: referencePost.publisher.picture?.id, | ||||
|                 radius: 16, | ||||
|               ), | ||||
|               const SizedBox(width: 8), | ||||
|               Expanded( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     Text( | ||||
|                       referencePost.publisher.nick, | ||||
|                       style: const TextStyle( | ||||
|                         fontWeight: FontWeight.bold, | ||||
|                         fontSize: 14, | ||||
|                       ), | ||||
|                     ), | ||||
|                     if (referencePost.visibility != 0) | ||||
|                       Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           Icon( | ||||
|                             PostVisibilityHelpers.getVisibilityIcon( | ||||
|                               referencePost.visibility, | ||||
|                             ), | ||||
|                             size: 12, | ||||
|                             color: Theme.of(context).colorScheme.secondary, | ||||
|                           ), | ||||
|                           const SizedBox(width: 4), | ||||
|                           Text( | ||||
|                             PostVisibilityHelpers.getVisibilityText( | ||||
|                               referencePost.visibility, | ||||
|                             ).tr(), | ||||
|                             style: TextStyle( | ||||
|                               fontSize: 10, | ||||
|                               color: Theme.of(context).colorScheme.secondary, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ).padding(top: 2, bottom: 2), | ||||
|                     if (referencePost.title?.isNotEmpty ?? false) | ||||
|                       Text( | ||||
|                         referencePost.title!, | ||||
|                         style: TextStyle( | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                           fontSize: 13, | ||||
|                           color: Theme.of(context).colorScheme.onSurface, | ||||
|                         ), | ||||
|                       ).padding(top: 2, bottom: 2), | ||||
|                     if (referencePost.description?.isNotEmpty ?? false) | ||||
|                       Text( | ||||
|                         referencePost.description!, | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 12, | ||||
|                           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                         ), | ||||
|                         maxLines: 2, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                       ).padding(bottom: 2), | ||||
|                     if (referencePost.content?.isNotEmpty ?? false) | ||||
|                       MarkdownTextContent( | ||||
|                         content: referencePost.content!, | ||||
|                         textStyle: const TextStyle(fontSize: 14), | ||||
|                         isSelectable: false, | ||||
|                         linesMargin: | ||||
|                             referencePost.type == 0 | ||||
|                                 ? const EdgeInsets.only(bottom: 4) | ||||
|                                 : null, | ||||
|                         attachments: item.attachments, | ||||
|                       ).padding(bottom: 4), | ||||
|                     if (referencePost.isTruncated) | ||||
|                       const PostTruncateHint( | ||||
|                         isCompact: true, | ||||
|                         margin: EdgeInsets.only(top: 4, bottom: 8), | ||||
|                       ), | ||||
|                     if (referencePost.attachments.isNotEmpty && | ||||
|                         referencePost.type != 1) | ||||
|                       Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           Icon( | ||||
|                             Symbols.attach_file, | ||||
|                             size: 12, | ||||
|                             color: Theme.of(context).colorScheme.secondary, | ||||
|                           ), | ||||
|                           const SizedBox(width: 4), | ||||
|                           Text( | ||||
|                             'postHasAttachments'.plural( | ||||
|                               referencePost.attachments.length, | ||||
|                             ), | ||||
|                             style: TextStyle( | ||||
|                               color: Theme.of(context).colorScheme.secondary, | ||||
|                               fontSize: 12, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ).padding(vertical: 2), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     if (!isInteractive) { | ||||
|       return content; | ||||
|     } | ||||
|  | ||||
|     return content.gestures( | ||||
|       onTap: | ||||
|           () => context.pushNamed( | ||||
|             'postDetail', | ||||
|             pathParameters: {'id': referencePost.id}, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostHeader extends StatelessWidget { | ||||
|   final SnPost item; | ||||
|   final bool isFullPost; | ||||
|   final Widget? trailing; | ||||
|   final bool isInteractive; | ||||
|   final EdgeInsets renderingPadding; | ||||
|  | ||||
|   const PostHeader({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.isFullPost = false, | ||||
|     this.trailing, | ||||
|     this.isInteractive = true, | ||||
|     this.renderingPadding = EdgeInsets.zero, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Row( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       spacing: 12, | ||||
|       children: [ | ||||
|         GestureDetector( | ||||
|           onTap: | ||||
|               isInteractive | ||||
|                   ? () { | ||||
|                     context.pushNamed( | ||||
|                       'publisherProfile', | ||||
|                       pathParameters: {'name': item.publisher.name}, | ||||
|                     ); | ||||
|                   } | ||||
|                   : null, | ||||
|           child: ProfilePictureWidget(file: item.publisher.picture, radius: 16), | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   Text(item.publisher.nick).bold(), | ||||
|                   if (item.publisher.verification != null) | ||||
|                     VerificationMark(mark: item.publisher.verification!), | ||||
|                   Text('@${item.publisher.name}').fontSize(11), | ||||
|                 ], | ||||
|               ), | ||||
|               Row( | ||||
|                 spacing: 6, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     isFullPost | ||||
|                         ? (item.publishedAt ?? item.createdAt)!.formatSystem() | ||||
|                         : (item.publishedAt ?? item.createdAt)!.formatRelative( | ||||
|                           context, | ||||
|                         ), | ||||
|                   ).fontSize(10), | ||||
|                   if (item.editedAt != null) | ||||
|                     Text( | ||||
|                       'editedAt'.tr(args: [item.editedAt!.formatSystem()]), | ||||
|                     ).fontSize(10), | ||||
|                   if (item.visibility != 0) | ||||
|                     Text( | ||||
|                       PostVisibilityHelpers.getVisibilityText( | ||||
|                         item.visibility, | ||||
|                       ).tr(), | ||||
|                     ).fontSize(10), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         if (trailing != null) trailing!, | ||||
|       ], | ||||
|     ).padding(horizontal: renderingPadding.horizontal, bottom: 4); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostBody extends ConsumerWidget { | ||||
|   final SnPost item; | ||||
|   final bool isFullPost; | ||||
|   final bool isTextSelectable; | ||||
|   final Widget? translationSection; | ||||
|   final bool isInteractive; | ||||
|   final EdgeInsets renderingPadding; | ||||
|  | ||||
|   const PostBody({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.isFullPost = false, | ||||
|     this.isTextSelectable = true, | ||||
|     this.translationSection, | ||||
|     this.isInteractive = true, | ||||
|     this.renderingPadding = EdgeInsets.zero, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         if (!isFullPost && item.type == 1) | ||||
|           Container( | ||||
|             decoration: BoxDecoration( | ||||
|               border: Border.all( | ||||
|                 color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||
|               ), | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|             ), | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|             margin: const EdgeInsets.only(top: 4), | ||||
|             child: Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Align( | ||||
|                   alignment: Alignment.centerLeft, | ||||
|                   child: Badge( | ||||
|                     label: const Text('postArticle').tr(), | ||||
|                     backgroundColor: Theme.of(context).colorScheme.primary, | ||||
|                     textColor: Theme.of(context).colorScheme.onPrimary, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 if (item.title != null) | ||||
|                   Text( | ||||
|                     item.title!, | ||||
|                     style: Theme.of(context).textTheme.titleMedium!.copyWith( | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                     ), | ||||
|                   ), | ||||
|                 if (item.description != null) | ||||
|                   Text( | ||||
|                     item.description!, | ||||
|                     style: Theme.of(context).textTheme.bodyMedium, | ||||
|                   ) | ||||
|                 else | ||||
|                   MarkdownTextContent(content: '${item.content!}...'), | ||||
|               ], | ||||
|             ), | ||||
|           ) | ||||
|         else if ((item.content?.isNotEmpty ?? false) || | ||||
|             (item.title?.isNotEmpty ?? false) || | ||||
|             (item.description?.isNotEmpty ?? false)) | ||||
|           Padding( | ||||
|             padding: EdgeInsets.only( | ||||
|               left: renderingPadding.horizontal, | ||||
|               right: renderingPadding.horizontal, | ||||
|             ), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 if ((item.title?.isNotEmpty ?? false) || | ||||
|                     (item.description?.isNotEmpty ?? false)) | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       if (item.title?.isNotEmpty ?? false) | ||||
|                         Text( | ||||
|                           item.title!, | ||||
|                           style: Theme.of(context).textTheme.titleMedium! | ||||
|                               .copyWith(fontWeight: FontWeight.bold), | ||||
|                         ), | ||||
|                       if (item.description?.isNotEmpty ?? false) | ||||
|                         Text( | ||||
|                           item.description!, | ||||
|                           style: Theme.of(context).textTheme.bodyMedium, | ||||
|                         ), | ||||
|                     ], | ||||
|                   ).padding(bottom: 4), | ||||
|                 MarkdownTextContent( | ||||
|                   content: | ||||
|                       item.isTruncated ? '${item.content!}...' : item.content!, | ||||
|                   isSelectable: isTextSelectable, | ||||
|                 ), | ||||
|                 if (translationSection != null) translationSection!, | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         if (item.isTruncated && item.type != 1) | ||||
|           PostTruncateHint( | ||||
|             isCompact: true, | ||||
|             withArrow: isInteractive, | ||||
|             margin: EdgeInsets.only( | ||||
|               top: 4, | ||||
|               bottom: 4, | ||||
|               left: renderingPadding.horizontal, | ||||
|               right: renderingPadding.horizontal, | ||||
|             ), | ||||
|           ), | ||||
|         if (item.attachments.isNotEmpty && item.type != 1) | ||||
|           CloudFileList( | ||||
|             files: item.attachments, | ||||
|             padding: EdgeInsets.symmetric( | ||||
|               horizontal: renderingPadding.horizontal, | ||||
|               vertical: 4, | ||||
|             ), | ||||
|           ), | ||||
|         if (item.tags.isNotEmpty || item.categories.isNotEmpty) | ||||
|           Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             spacing: 2, | ||||
|             children: [ | ||||
|               if (item.tags.isNotEmpty) | ||||
|                 Wrap( | ||||
|                   runAlignment: WrapAlignment.center, | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.label, size: 16).padding(top: 2), | ||||
|                     for (final tag | ||||
|                         in isFullPost ? item.tags : item.tags.take(3)) | ||||
|                       InkWell( | ||||
|                         onTap: | ||||
|                             isInteractive | ||||
|                                 ? () { | ||||
|                                   GoRouter.of(context).pushNamed( | ||||
|                                     'postTagDetail', | ||||
|                                     pathParameters: {'slug': tag.slug}, | ||||
|                                   ); | ||||
|                                 } | ||||
|                                 : null, | ||||
|                         child: Text('#${tag.name ?? tag.slug}'), | ||||
|                       ), | ||||
|                     if (!isFullPost && item.tags.length > 3) | ||||
|                       Text('+${item.tags.length - 3}').opacity(0.6), | ||||
|                   ], | ||||
|                 ), | ||||
|               if (item.categories.isNotEmpty) | ||||
|                 Wrap( | ||||
|                   runAlignment: WrapAlignment.center, | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.category, size: 16).padding(top: 2), | ||||
|                     for (final category | ||||
|                         in isFullPost | ||||
|                             ? item.categories | ||||
|                             : item.categories.take(2)) | ||||
|                       InkWell( | ||||
|                         onTap: | ||||
|                             isInteractive | ||||
|                                 ? () { | ||||
|                                   GoRouter.of(context).pushNamed( | ||||
|                                     'postCategoryDetail', | ||||
|                                     pathParameters: {'slug': category.slug}, | ||||
|                                   ); | ||||
|                                 } | ||||
|                                 : null, | ||||
|                         child: Text(category.categoryDisplayTitle), | ||||
|                       ), | ||||
|                     if (!isFullPost && item.categories.length > 2) | ||||
|                       Text('+${item.categories.length - 2}').opacity(0.6), | ||||
|                   ], | ||||
|                 ), | ||||
|             ], | ||||
|           ).padding(horizontal: renderingPadding.horizontal + 4, top: 4), | ||||
|         if (item.meta?['embeds'] != null) | ||||
|           ...((item.meta!['embeds'] as List<dynamic>) | ||||
|               .map((embedData) => convertMapKeysToSnakeCase(embedData)) | ||||
|               .map( | ||||
|                 (embedData) => switch (embedData['type']) { | ||||
|                   'link' => EmbedLinkWidget( | ||||
|                     link: SnScrappedLink.fromJson(embedData), | ||||
|                     maxWidth: math.min( | ||||
|                       MediaQuery.of(context).size.width, | ||||
|                       kWideScreenWidth, | ||||
|                     ), | ||||
|                     margin: EdgeInsets.only( | ||||
|                       top: 4, | ||||
|                       bottom: 4, | ||||
|                       left: renderingPadding.horizontal, | ||||
|                       right: renderingPadding.horizontal, | ||||
|                     ), | ||||
|                   ), | ||||
|                   'poll' => Card( | ||||
|                     margin: EdgeInsets.symmetric( | ||||
|                       horizontal: renderingPadding.horizontal, | ||||
|                       vertical: 8, | ||||
|                     ), | ||||
|                     child: | ||||
|                         embedData['poll'] == null | ||||
|                             ? const Text('Poll was not loaded...') | ||||
|                             : PollSubmit( | ||||
|                               initialAnswers: | ||||
|                                   embedData['poll']?['user_answer']?['answer'], | ||||
|                               stats: embedData['poll']?['stats'], | ||||
|                               poll: SnPollWithStats.fromJson(embedData['poll']), | ||||
|                               onSubmit: (_) {}, | ||||
|                               isReadonly: !isInteractive, | ||||
|                             ).padding(horizontal: 16, vertical: 12), | ||||
|                   ), | ||||
|                   _ => Text('Unable show embed: ${embedData['type']}'), | ||||
|                 }, | ||||
|               )), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'post_item.dart'; | ||||
| part of 'post_shared.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| @@ -2021,6 +2021,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|   screenshot: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: screenshot | ||||
|       sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.0" | ||||
|   scroll_to_index: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -137,6 +137,7 @@ dependencies: | ||||
|   firebase_crashlytics: ^5.0.0 | ||||
|   firebase_analytics: ^12.0.0 | ||||
|   material_color_utilities: ^0.11.1 | ||||
|   screenshot: ^3.0.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user