Compare commits
	
		
			1 Commits
		
	
	
		
			3.2.0+124
			...
			8236d31ecc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8236d31ecc | 
| @@ -573,6 +573,7 @@ | |||||||
|   "keyboardShortcuts": "Keyboard Shortcuts", |   "keyboardShortcuts": "Keyboard Shortcuts", | ||||||
|   "share": "Share", |   "share": "Share", | ||||||
|   "sharePost": "Share Post", |   "sharePost": "Share Post", | ||||||
|  |   "sharePostPhoto": "Share Post as Photo", | ||||||
|   "quickActions": "Quick Actions", |   "quickActions": "Quick Actions", | ||||||
|   "post": "Post", |   "post": "Post", | ||||||
|   "copy": "Copy", |   "copy": "Copy", | ||||||
|   | |||||||
| @@ -572,6 +572,7 @@ | |||||||
|   "postContentEmpty": "发布的内容不能为空", |   "postContentEmpty": "发布的内容不能为空", | ||||||
|   "share": "分享", |   "share": "分享", | ||||||
|   "sharePost": "分享帖子", |   "sharePost": "分享帖子", | ||||||
|  |   "sharePostAsPhoto": "通过图片分享帖子", | ||||||
|   "quickActions": "快捷操作", |   "quickActions": "快捷操作", | ||||||
|   "post": "帖子", |   "post": "帖子", | ||||||
|   "copy": "复制", |   "copy": "复制", | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ class PollSubmit extends ConsumerStatefulWidget { | |||||||
|     this.initialAnswers, |     this.initialAnswers, | ||||||
|     this.onCancel, |     this.onCancel, | ||||||
|     this.showProgress = true, |     this.showProgress = true, | ||||||
|  |     this.isReadonly = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   final SnPollWithStats poll; |   final SnPollWithStats poll; | ||||||
| @@ -31,6 +32,8 @@ class PollSubmit extends ConsumerStatefulWidget { | |||||||
|   /// Whether to show a progress indicator (e.g., "2 / N"). |   /// Whether to show a progress indicator (e.g., "2 / N"). | ||||||
|   final bool showProgress; |   final bool showProgress; | ||||||
|  |  | ||||||
|  |   final bool isReadonly; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   ConsumerState<PollSubmit> createState() => _PollSubmitState(); |   ConsumerState<PollSubmit> createState() => _PollSubmitState(); | ||||||
| } | } | ||||||
| @@ -59,7 +62,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|     _questions = [...widget.poll.questions] |     _questions = [...widget.poll.questions] | ||||||
|       ..sort((a, b) => a.order.compareTo(b.order)); |       ..sort((a, b) => a.order.compareTo(b.order)); | ||||||
|     _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); |     _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); | ||||||
|     _loadCurrentIntoLocalState(); |     if (!widget.isReadonly) { | ||||||
|  |       _loadCurrentIntoLocalState(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -74,7 +79,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|           [...widget.poll.questions] |           [...widget.poll.questions] | ||||||
|             ..sort((a, b) => a.order.compareTo(b.order)), |             ..sort((a, b) => a.order.compareTo(b.order)), | ||||||
|         ); |         ); | ||||||
|       _loadCurrentIntoLocalState(); |       if (!widget.isReadonly) { | ||||||
|  |         _loadCurrentIntoLocalState(); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -259,10 +266,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|                     child: Text( |                     child: Text( | ||||||
|                       widget.poll.description!, |                       widget.poll.description!, | ||||||
|                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( |                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|                         color: Theme.of( |                             color: Theme.of( | ||||||
|                           context, |                               context, | ||||||
|                         ).textTheme.bodyMedium?.color?.withOpacity(0.7), |                             ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||||
|                       ), |                           ), | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|               ], |               ], | ||||||
| @@ -287,8 +294,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|                 child: Text( |                 child: Text( | ||||||
|                   '*', |                   '*', | ||||||
|                   style: Theme.of(context).textTheme.titleMedium?.copyWith( |                   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( |             child: Text( | ||||||
|               q.description!, |               q.description!, | ||||||
|               style: Theme.of(context).textTheme.bodySmall?.copyWith( |               style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|                 color: Theme.of( |                     color: Theme.of( | ||||||
|                   context, |                       context, | ||||||
|                 ).textTheme.bodySmall?.color?.withOpacity(0.7), |                     ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||||
|               ), |                   ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|       ], |       ], | ||||||
| @@ -340,14 +347,12 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|       case SnPollQuestionType.yesNo: |       case SnPollQuestionType.yesNo: | ||||||
|         // yes/no: map {true: count, false: count} |         // yes/no: map {true: count, false: count} | ||||||
|         if (raw is Map) { |         if (raw is Map) { | ||||||
|           final int yes = |           final int yes = (raw[true] is int) | ||||||
|               (raw[true] is int) |               ? raw[true] as int | ||||||
|                   ? raw[true] as int |               : int.tryParse('${raw[true]}') ?? 0; | ||||||
|                   : int.tryParse('${raw[true]}') ?? 0; |           final int no = (raw[false] is int) | ||||||
|           final int no = |               ? raw[false] as int | ||||||
|               (raw[false] is int) |               : int.tryParse('${raw[false]}') ?? 0; | ||||||
|                   ? raw[false] as int |  | ||||||
|                   : int.tryParse('${raw[false]}') ?? 0; |  | ||||||
|           final total = (yes + no).clamp(0, 1 << 31); |           final total = (yes + no).clamp(0, 1 << 31); | ||||||
|           final yesPct = total == 0 ? 0.0 : yes / total; |           final yesPct = total == 0 ? 0.0 : yes / total; | ||||||
|           final noPct = total == 0 ? 0.0 : no / total; |           final noPct = total == 0 ? 0.0 : no / total; | ||||||
| @@ -415,8 +420,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|                 Text( |                 Text( | ||||||
|                   'Total: $total', |                   'Total: $total', | ||||||
|                   style: Theme.of(context).textTheme.labelSmall?.copyWith( |                   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( |               Text( | ||||||
|                 'Stats', |                 'Stats', | ||||||
|                 style: Theme.of(context).textTheme.labelLarge?.copyWith( |                 style: Theme.of(context).textTheme.labelLarge?.copyWith( | ||||||
|                   color: Theme.of(context).colorScheme.onSurfaceVariant, |                       color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|                 ), |                     ), | ||||||
|               ), |               ), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               body, |               body, | ||||||
| @@ -577,14 +582,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|         ), |         ), | ||||||
|         const Spacer(), |         const Spacer(), | ||||||
|         FilledButton.icon( |         FilledButton.icon( | ||||||
|           icon: |           icon: _submitting | ||||||
|               _submitting |               ? const SizedBox( | ||||||
|                   ? const SizedBox( |                   width: 16, | ||||||
|                     width: 16, |                   height: 16, | ||||||
|                     height: 16, |                   child: CircularProgressIndicator(strokeWidth: 2), | ||||||
|                     child: CircularProgressIndicator(strokeWidth: 2), |                 ) | ||||||
|                   ) |               : Icon(isLast ? Icons.check : Icons.arrow_forward), | ||||||
|                   : Icon(isLast ? Icons.check : Icons.arrow_forward), |  | ||||||
|           label: Text(isLast ? 'Submit' : 'Next'), |           label: Text(isLast ? 'Submit' : 'Next'), | ||||||
|           onPressed: canProceed ? _next : null, |           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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     if (_questions.isEmpty) { |     if (_questions.isEmpty) { | ||||||
|       return const SizedBox.shrink(); |       return const SizedBox.shrink(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (widget.isReadonly) { | ||||||
|  |       return _buildReadonlyView(context); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, |       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|       children: [ |       children: [ | ||||||
| @@ -647,10 +731,9 @@ class _BarStatRow extends StatelessWidget { | |||||||
|     final bgColor = Theme.of( |     final bgColor = Theme.of( | ||||||
|       context, |       context, | ||||||
|     ).colorScheme.surfaceVariant.withOpacity(0.6); |     ).colorScheme.surfaceVariant.withOpacity(0.6); | ||||||
|     final fg = |     final fg = (fraction.isNaN || fraction.isInfinite) | ||||||
|         (fraction.isNaN || fraction.isInfinite) |         ? 0.0 | ||||||
|             ? 0.0 |         : fraction.clamp(0.0, 1.0); | ||||||
|             : fraction.clamp(0.0, 1.0); |  | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       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/pods/network.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/widgets/alert.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_item.dart'; | ||||||
|  | import 'package:island/widgets/post/post_shared.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; |  | ||||||
| import 'package:super_context_menu/super_context_menu.dart'; | import 'package:super_context_menu/super_context_menu.dart'; | ||||||
|  |  | ||||||
| class PostItemCreator extends HookConsumerWidget { | class PostItemCreator extends HookConsumerWidget { | ||||||
| @@ -81,7 +79,6 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|               title: 'copyLink'.tr(), |               title: 'copyLink'.tr(), | ||||||
|               image: MenuImage.icon(Symbols.link), |               image: MenuImage.icon(Symbols.link), | ||||||
|               callback: () { |               callback: () { | ||||||
|                 // Copy post link to clipboard |  | ||||||
|                 context.pushNamed( |                 context.pushNamed( | ||||||
|                   'postDetail', |                   'postDetail', | ||||||
|                   pathParameters: {'id': item.id}, |                   pathParameters: {'id': item.id}, | ||||||
| @@ -105,8 +102,9 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|             child: Column( |             child: Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|                 _buildPostHeader(context), |                 PostHeader(item: item), | ||||||
|                 _buildPostContent(context), |                 PostBody(item: item), | ||||||
|  |                 ReferencedPostWidget(item: item), | ||||||
|                 const Gap(16), |                 const Gap(16), | ||||||
|                 _buildAnalyticsSection(context), |                 _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) { |   Widget _buildAnalyticsSection(BuildContext context) { | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: [ |       children: [ | ||||||
|         Text('Analytics', style: Theme.of(context).textTheme.titleSmall), |         Text('Analytics', style: Theme.of(context).textTheme.titleSmall), | ||||||
|         const Gap(8), |         const Gap(8), | ||||||
|  |  | ||||||
|         // Engagement metrics in a card |  | ||||||
|         Card( |         Card( | ||||||
|           elevation: 1, |           elevation: 1, | ||||||
|           margin: EdgeInsets.zero, |           margin: EdgeInsets.zero, | ||||||
| @@ -279,15 +161,9 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|         const Gap(16), |         const Gap(16), | ||||||
|  |  | ||||||
|         // Reactions summary |  | ||||||
|         if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), |         if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), | ||||||
|  |  | ||||||
|         // Metadata section |  | ||||||
|         if (item.meta != null && item.meta!.isNotEmpty) |         if (item.meta != null && item.meta!.isNotEmpty) | ||||||
|           _buildMetadataSection(context), |           _buildMetadataSection(context), | ||||||
|  |  | ||||||
|         // Creation and modification timestamps |  | ||||||
|         const Gap(16), |         const Gap(16), | ||||||
|         Row( |         Row( | ||||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, |           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 | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'post_item.dart'; | part of 'post_shared.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| @@ -2021,6 +2021,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     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: |   scroll_to_index: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -137,6 +137,7 @@ dependencies: | |||||||
|   firebase_crashlytics: ^5.0.0 |   firebase_crashlytics: ^5.0.0 | ||||||
|   firebase_analytics: ^12.0.0 |   firebase_analytics: ^12.0.0 | ||||||
|   material_color_utilities: ^0.11.1 |   material_color_utilities: ^0.11.1 | ||||||
|  |   screenshot: ^3.0.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user