💄 Optimized post list
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -87,28 +88,70 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|  | ||||
|                   final scrollProgress = | ||||
|                       (scrollOffset / colorChangeOffset).clamp(0.0, 1.0); | ||||
|                   final backgroundColor = Color.lerp( | ||||
|                     Theme.of(context) | ||||
|                         .colorScheme | ||||
|                         .surfaceContainerLow | ||||
|                         .withOpacity(0), | ||||
|                     Theme.of(context) | ||||
|                         .colorScheme | ||||
|                         .surfaceContainerLow | ||||
|                         .withOpacity(0.9), | ||||
|                     scrollProgress, | ||||
|                   ); | ||||
|                   final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0; | ||||
|  | ||||
|                   return SliverAppBar( | ||||
|                     backgroundColor: backgroundColor, | ||||
|                     flexibleSpace: SizedBox( | ||||
|                       height: 48, | ||||
|                       child: const Row( | ||||
|                         children: [ | ||||
|                           RealmSwitcher(), | ||||
|                         ], | ||||
|                       ).paddingSymmetric(horizontal: 8), | ||||
|                     ).paddingOnly(top: MediaQuery.of(context).padding.top), | ||||
|                     flexibleSpace: ClipRRect( | ||||
|                       child: BackdropFilter( | ||||
|                         filter: ImageFilter.blur( | ||||
|                           sigmaX: blurSigma, | ||||
|                           sigmaY: blurSigma, | ||||
|                         ), | ||||
|                         child: ListView( | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           physics: const NeverScrollableScrollPhysics(), | ||||
|                           children: [ | ||||
|                             SizedBox( | ||||
|                               height: 48, | ||||
|                               child: const Row( | ||||
|                                 children: [ | ||||
|                                   RealmSwitcher(), | ||||
|                                 ], | ||||
|                               ).paddingSymmetric(horizontal: 8), | ||||
|                             ), | ||||
|                             TabBar( | ||||
|                               controller: _tabController, | ||||
|                               dividerHeight: 0.3, | ||||
|                               tabAlignment: TabAlignment.fill, | ||||
|                               tabs: [ | ||||
|                                 Tab( | ||||
|                                   child: Row( | ||||
|                                     mainAxisSize: MainAxisSize.min, | ||||
|                                     children: [ | ||||
|                                       const Icon(Icons.feed, size: 20), | ||||
|                                       const Gap(8), | ||||
|                                       Text('postListNews'.tr), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 Tab( | ||||
|                                   child: Row( | ||||
|                                     mainAxisSize: MainAxisSize.min, | ||||
|                                     children: [ | ||||
|                                       const Icon(Icons.people, size: 20), | ||||
|                                       const Gap(8), | ||||
|                                       Text('postListFriends'.tr), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 Tab( | ||||
|                                   child: Row( | ||||
|                                     mainAxisSize: MainAxisSize.min, | ||||
|                                     children: [ | ||||
|                                       const Icon(Icons.shuffle_on_outlined, | ||||
|                                           size: 20), | ||||
|                                       const Gap(8), | ||||
|                                       Text('postListShuffle'.tr), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ], | ||||
|                         ).paddingOnly(top: MediaQuery.of(context).padding.top), | ||||
|                       ), | ||||
|                     ), | ||||
|                     expandedHeight: 96, | ||||
|                     snap: true, | ||||
|                     floating: true, | ||||
|                     toolbarHeight: AppTheme.toolbarHeight(context), | ||||
| @@ -120,43 +163,6 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|                         width: AppTheme.isLargeScreen(context) ? 8 : 16, | ||||
|                       ), | ||||
|                     ], | ||||
|                     bottom: TabBar( | ||||
|                       controller: _tabController, | ||||
|                       dividerHeight: 0.3, | ||||
|                       tabAlignment: TabAlignment.fill, | ||||
|                       tabs: [ | ||||
|                         Tab( | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               const Icon(Icons.feed, size: 20), | ||||
|                               const Gap(8), | ||||
|                               Text('postListNews'.tr), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         Tab( | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               const Icon(Icons.people, size: 20), | ||||
|                               const Gap(8), | ||||
|                               Text('postListFriends'.tr), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         Tab( | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               const Icon(Icons.shuffle_on_outlined, size: 20), | ||||
|                               const Gap(8), | ||||
|                               Text('postListShuffle'.tr), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|               ) | ||||
| @@ -180,6 +186,12 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|                         onRefresh: () => _postController.reloadAllOver(), | ||||
|                         child: CustomScrollView(slivers: [ | ||||
|                           ControlledPostListWidget( | ||||
|                             padding: AppTheme.isLargeScreen(context) | ||||
|                                 ? EdgeInsets.symmetric( | ||||
|                                     horizontal: 4, | ||||
|                                     vertical: 8, | ||||
|                                   ) | ||||
|                                 : EdgeInsets.zero, | ||||
|                             controller: _postController.pagingController, | ||||
|                             onUpdate: () => _postController.reloadAllOver(), | ||||
|                           ), | ||||
| @@ -191,6 +203,9 @@ class _ExploreScreenState extends State<ExploreScreen> | ||||
|                             onRefresh: () => _postController.reloadAllOver(), | ||||
|                             child: CustomScrollView(slivers: [ | ||||
|                               ControlledPostListWidget( | ||||
|                                 padding: AppTheme.isLargeScreen(context) | ||||
|                                     ? EdgeInsets.symmetric(horizontal: 16) | ||||
|                                     : EdgeInsets.zero, | ||||
|                                 controller: _postController.pagingController, | ||||
|                                 onUpdate: () => _postController.reloadAllOver(), | ||||
|                               ), | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:solian/exts.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/content/posts.dart'; | ||||
| import 'package:solian/providers/last_read.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| import 'package:solian/widgets/posts/post_item.dart'; | ||||
| import 'package:solian/widgets/posts/post_replies.dart'; | ||||
|  | ||||
| @@ -67,11 +68,18 @@ class _PostDetailScreenState extends State<PostDetailScreen> { | ||||
|                 isFullContent: true, | ||||
|                 isShowReply: false, | ||||
|                 isContentSelectable: true, | ||||
|                 padding: AppTheme.isLargeScreen(context) | ||||
|                     ? EdgeInsets.symmetric( | ||||
|                         horizontal: 4, | ||||
|                         vertical: 8, | ||||
|                       ) | ||||
|                     : EdgeInsets.zero, | ||||
|               ), | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: | ||||
|                   const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4), | ||||
|               child: const Divider(thickness: 0.3, height: 1).paddingOnly( | ||||
|                 top: 8, | ||||
|               ), | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: Align( | ||||
| @@ -82,7 +90,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> { | ||||
|                 ).paddingOnly(left: 24, right: 24, top: 16), | ||||
|               ), | ||||
|             ), | ||||
|             PostReplyList(item: item!), | ||||
|             PostReplyList( | ||||
|               item: item!, | ||||
|               padding: AppTheme.isLargeScreen(context) | ||||
|                   ? EdgeInsets.symmetric( | ||||
|                       horizontal: 4, | ||||
|                       vertical: 8, | ||||
|                     ) | ||||
|                   : EdgeInsets.zero, | ||||
|             ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: SizedBox(height: MediaQuery.of(context).padding.bottom), | ||||
|             ), | ||||
|   | ||||
| @@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> { | ||||
|  | ||||
|     return SizedBox( | ||||
|       height: MediaQuery.of(context).size.height * 0.75, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       child: ListView( | ||||
|         children: [ | ||||
|           AccountHeadingWidget( | ||||
|             avatar: _userinfo!.avatar, | ||||
|   | ||||
| @@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget { | ||||
|           ), | ||||
|           if (showBadge && badge != null) | ||||
|             Positioned( | ||||
|               right: 12, | ||||
|               bottom: 8, | ||||
|               right: 8, | ||||
|               bottom: 4, | ||||
|               child: Material( | ||||
|                 color: Colors.transparent, | ||||
|                 child: Chip(label: Text(badge!)), | ||||
|                 child: Chip( | ||||
|                   label: Text(badge!), | ||||
|                   labelStyle: GoogleFonts.robotoMono(), | ||||
|                   visualDensity: const VisualDensity( | ||||
|                     horizontal: -4, | ||||
|                     vertical: -2, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           if (showHideButton && item.isMature) | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:carousel_slider/carousel_slider.dart'; | ||||
| import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:flutter/material.dart' hide CarouselController; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| @@ -23,6 +22,7 @@ class AttachmentList extends StatefulWidget { | ||||
|   final bool autoload; | ||||
|   final double columnMaxWidth; | ||||
|  | ||||
|   final EdgeInsets? padding; | ||||
|   final double? width; | ||||
|   final double? viewport; | ||||
|  | ||||
| @@ -36,6 +36,7 @@ class AttachmentList extends StatefulWidget { | ||||
|     this.isFullWidth = false, | ||||
|     this.autoload = false, | ||||
|     this.columnMaxWidth = 480, | ||||
|     this.padding, | ||||
|     this.width, | ||||
|     this.viewport, | ||||
|   }); | ||||
| @@ -161,9 +162,7 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|             color: _unFocusColor, | ||||
|           ).paddingOnly(right: 5), | ||||
|           Text( | ||||
|             'attachmentHint'.trParams( | ||||
|               {'count': _attachments.toString()}, | ||||
|             ), | ||||
|             'attachmentHint'.trParams({'count': _attachments.toString()}), | ||||
|             style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|           ) | ||||
|         ], | ||||
| @@ -179,8 +178,8 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|       final element = _attachments.first; | ||||
|       double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; | ||||
|       return Container( | ||||
|         width: MediaQuery.of(context).size.width, | ||||
|         constraints: BoxConstraints( | ||||
|           maxWidth: widget.columnMaxWidth, | ||||
|           maxHeight: 640, | ||||
|         ), | ||||
|         child: AspectRatio( | ||||
| @@ -271,26 +270,26 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return SizedBox( | ||||
|       width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth), | ||||
|       child: CarouselSlider.builder( | ||||
|         options: CarouselOptions( | ||||
|           disableCenter: true, | ||||
|           animateToClosest: true, | ||||
|           aspectRatio: _aspectRatio, | ||||
|           enlargeCenterPage: true, | ||||
|           viewportFraction: widget.viewport ?? 0.95, | ||||
|           enableInfiniteScroll: false, | ||||
|         ), | ||||
|     return Container( | ||||
|       constraints: BoxConstraints( | ||||
|         maxHeight: 320, | ||||
|       ), | ||||
|       child: ListView.separated( | ||||
|         padding: widget.padding, | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         shrinkWrap: true, | ||||
|         itemCount: _attachments.length, | ||||
|         itemBuilder: (context, idx, _) { | ||||
|         itemBuilder: (context, idx) { | ||||
|           final element = _attachments[idx]; | ||||
|           if (element == null) const SizedBox.shrink(); | ||||
|           double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; | ||||
|           final ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; | ||||
|           return Container( | ||||
|             constraints: BoxConstraints( | ||||
|               maxWidth: widget.columnMaxWidth, | ||||
|               maxHeight: 640, | ||||
|               maxWidth: math.min( | ||||
|                 widget.columnMaxWidth, | ||||
|                 MediaQuery.of(context).size.width - | ||||
|                     (widget.padding?.horizontal ?? 0), | ||||
|               ), | ||||
|             ), | ||||
|             child: AspectRatio( | ||||
|               aspectRatio: ratio, | ||||
| @@ -310,6 +309,7 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|         separatorBuilder: (context, _) => const Gap(8), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -2,15 +2,21 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:flutter_svg/svg.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/models/link.dart'; | ||||
| import 'package:solian/providers/link_expander.dart'; | ||||
| import 'package:solian/widgets/auto_cache_image.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class LinkExpansion extends StatelessWidget { | ||||
| class LinkExpansion extends StatefulWidget { | ||||
|   final String content; | ||||
|  | ||||
|   const LinkExpansion({super.key, required this.content}); | ||||
|  | ||||
|   @override | ||||
|   State<LinkExpansion> createState() => _LinkExpansionState(); | ||||
| } | ||||
|  | ||||
| class _LinkExpansionState extends State<LinkExpansion> { | ||||
|   Widget _buildImage(String url, {double? width, double? height}) { | ||||
|     if (url.endsWith('svg')) { | ||||
|       return SvgPicture.network(url, width: width, height: height); | ||||
| @@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   List<LinkMeta>? _meta; | ||||
|  | ||||
|   Future<void> _doExpand() async { | ||||
|     final linkRegex = RegExp( | ||||
|       r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]', | ||||
|     ); | ||||
|     final matches = linkRegex.allMatches(content); | ||||
|     if (matches.isEmpty) { | ||||
|       return const SizedBox.shrink(); | ||||
|     } | ||||
|     final matches = linkRegex.allMatches(widget.content); | ||||
|     if (matches.isEmpty) return; | ||||
|  | ||||
|     final LinkExpandProvider expandController = Get.find(); | ||||
|  | ||||
|     if (matches.isEmpty) return; | ||||
|  | ||||
|     List<LinkMeta> out = List.empty(growable: true); | ||||
|     for (final x in matches) { | ||||
|       final result = await expandController.expandLink(x.group(0)!); | ||||
|       if (result != null) out.add(result); | ||||
|     } | ||||
|  | ||||
|     setState(() => _meta = out); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _doExpand(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (_meta?.isEmpty ?? true) return const SizedBox.shrink(); | ||||
|  | ||||
|     return Wrap( | ||||
|       children: matches.map((x) { | ||||
|       children: _meta!.map((x) { | ||||
|         return Container( | ||||
|           constraints: BoxConstraints( | ||||
|             maxWidth: matches.length == 1 ? 480 : 340, | ||||
|             maxWidth: _meta!.length == 1 ? 480 : 340, | ||||
|           ), | ||||
|           child: FutureBuilder( | ||||
|             future: expandController.expandLink(x.group(0)!), | ||||
|             builder: (context, snapshot) { | ||||
|               if (!snapshot.hasData) { | ||||
|                 return const SizedBox.shrink(); | ||||
|               } | ||||
|  | ||||
|           child: Builder( | ||||
|             builder: (context) { | ||||
|               final isRichDescription = [ | ||||
|                 'solsynth.dev', | ||||
|               ].contains(Uri.parse(snapshot.data!.url).host); | ||||
|               ].contains(Uri.parse(x.url).host); | ||||
|  | ||||
|               return GestureDetector( | ||||
|                 child: Card( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       if ([ | ||||
|                         (snapshot.data!.icon?.isNotEmpty ?? false), | ||||
|                         snapshot.data!.siteName != null | ||||
|                       ].any((x) => x)) | ||||
|                       if ([(x.icon?.isNotEmpty ?? false), x.siteName != null] | ||||
|                           .any((x) => x)) | ||||
|                         Row( | ||||
|                           children: [ | ||||
|                             if (snapshot.data!.icon?.isNotEmpty ?? false) | ||||
|                             if (x.icon?.isNotEmpty ?? false) | ||||
|                               ClipRRect( | ||||
|                                 borderRadius: const BorderRadius.all( | ||||
|                                   Radius.circular(8), | ||||
|                                 ), | ||||
|                                 child: _buildImage( | ||||
|                                   snapshot.data!.icon!, | ||||
|                                   x.icon!, | ||||
|                                   width: 32, | ||||
|                                   height: 32, | ||||
|                                 ), | ||||
|                               ).paddingOnly(right: 8), | ||||
|                             if (snapshot.data!.siteName != null) | ||||
|                             if (x.siteName != null) | ||||
|                               Expanded( | ||||
|                                 child: Text( | ||||
|                                   snapshot.data!.siteName!, | ||||
|                                   x.siteName!, | ||||
|                                   style: Theme.of(context).textTheme.labelLarge, | ||||
|                                   maxLines: 1, | ||||
|                                   overflow: TextOverflow.ellipsis, | ||||
| @@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget { | ||||
|                               ), | ||||
|                           ], | ||||
|                         ).paddingOnly( | ||||
|                           bottom: (snapshot.data!.icon?.isNotEmpty ?? false) | ||||
|                               ? 8 | ||||
|                               : 4, | ||||
|                           bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4, | ||||
|                         ), | ||||
|                       if (snapshot.data!.image != null && | ||||
|                           (snapshot.data!.image?.startsWith('http') ?? false)) | ||||
|                       if (x.image != null && | ||||
|                           (x.image?.startsWith('http') ?? false)) | ||||
|                         ClipRRect( | ||||
|                           borderRadius: const BorderRadius.all( | ||||
|                             Radius.circular(8), | ||||
|                           ), | ||||
|                           child: _buildImage( | ||||
|                             snapshot.data!.image!, | ||||
|                           ), | ||||
|                           child: _buildImage(x.image!), | ||||
|                         ).paddingOnly(bottom: 8), | ||||
|                       Text( | ||||
|                         snapshot.data!.title ?? 'No Title', | ||||
|                         x.title ?? 'No Title', | ||||
|                         maxLines: 1, | ||||
|                         overflow: TextOverflow.fade, | ||||
|                         style: Theme.of(context).textTheme.bodyLarge, | ||||
|                       ), | ||||
|                       if (snapshot.data!.description != null && | ||||
|                           isRichDescription) | ||||
|                         MarkdownBody(data: snapshot.data!.description!) | ||||
|                       else if (snapshot.data!.description != null) | ||||
|                       if (x.description != null && isRichDescription) | ||||
|                         MarkdownBody(data: x.description!) | ||||
|                       else if (x.description != null) | ||||
|                         Text( | ||||
|                           snapshot.data!.description!, | ||||
|                           x.description!, | ||||
|                           maxLines: 3, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                         ), | ||||
| @@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget { | ||||
|                   ).paddingAll(12), | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   launchUrlString(x.group(0)!); | ||||
|                   launchUrlString(x.url); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:get/get.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/content/posts.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/screens/posts/post_detail.dart'; | ||||
| import 'package:solian/shells/title_shell.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| @@ -34,7 +35,10 @@ class PostItem extends StatefulWidget { | ||||
|   final bool isContentSelectable; | ||||
|   final bool showFeaturedReply; | ||||
|   final String? attachmentParent; | ||||
|  | ||||
|   final EdgeInsets? padding; | ||||
|   final Color? backgroundColor; | ||||
|  | ||||
|   final Function? onComment; | ||||
|  | ||||
|   const PostItem({ | ||||
| @@ -51,6 +55,7 @@ class PostItem extends StatefulWidget { | ||||
|     this.isContentSelectable = false, | ||||
|     this.showFeaturedReply = false, | ||||
|     this.attachmentParent, | ||||
|     this.padding, | ||||
|     this.backgroundColor, | ||||
|     this.onComment, | ||||
|   }); | ||||
| @@ -126,9 +131,7 @@ class _PostItemState extends State<PostItem> { | ||||
|           LinkExpansion(content: item.body['content']).paddingOnly( | ||||
|             left: 8, | ||||
|             right: 8, | ||||
|             top: 4, | ||||
|           ), | ||||
|           _PostFooterWidget(item: item).paddingOnly(left: 12), | ||||
|           if (attachments.isNotEmpty) | ||||
|             Row( | ||||
|               children: [ | ||||
| @@ -149,9 +152,8 @@ class _PostItemState extends State<PostItem> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return OpenContainer( | ||||
|       tappable: widget.isClickable, | ||||
|       closedBuilder: (_, openContainer) => Column( | ||||
|     return GestureDetector( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           _PostThumbnail( | ||||
| @@ -220,18 +222,20 @@ class _PostItemState extends State<PostItem> { | ||||
|                   ), | ||||
|                 ), | ||||
|               _PostFooterWidget(item: item), | ||||
|               LinkExpansion(content: item.body['content']).paddingOnly(top: 4), | ||||
|               LinkExpansion(content: item.body['content']), | ||||
|             ], | ||||
|           ).paddingOnly( | ||||
|             right: 16, | ||||
|             left: 16, | ||||
|           ).paddingSymmetric( | ||||
|             horizontal: (widget.padding?.horizontal ?? 0) + 16, | ||||
|           ), | ||||
|           if (hasAttachment) const Gap(8), | ||||
|           _PostAttachmentWidget( | ||||
|             item: item, | ||||
|             padding: widget.padding, | ||||
|           ), | ||||
|           _PostAttachmentWidget(item: item), | ||||
|           if (widget.showFeaturedReply) | ||||
|             _PostFeaturedReplyWidget(item: item).paddingSymmetric( | ||||
|               horizontal: 12, | ||||
|               horizontal: (widget.padding?.horizontal ?? 0) + 12, | ||||
|             ), | ||||
|           if (widget.showFeaturedReply) const Gap(8), | ||||
|           if (widget.isShowReply || widget.isReactable) | ||||
|             PostQuickAction( | ||||
|               isShowReply: widget.isShowReply, | ||||
| @@ -249,22 +253,23 @@ class _PostItemState extends State<PostItem> { | ||||
|                 } | ||||
|               }, | ||||
|             ).paddingOnly( | ||||
|               left: 14, | ||||
|               right: 14, | ||||
|               top: 8, | ||||
|               left: (widget.padding?.left ?? 0) + 14, | ||||
|               right: (widget.padding?.right ?? 0) + 14, | ||||
|             ) | ||||
|         ], | ||||
|       ).paddingOnly( | ||||
|         top: widget.padding?.top ?? 0, | ||||
|         bottom: widget.padding?.bottom ?? 0, | ||||
|       ), | ||||
|       openBuilder: (_, __) => TitleShell( | ||||
|         title: 'postDetail'.tr, | ||||
|         child: PostDetailScreen( | ||||
|           id: item.id.toString(), | ||||
|           post: item, | ||||
|         ), | ||||
|       ), | ||||
|       closedElevation: 0, | ||||
|       openElevation: 0, | ||||
|       closedColor: Colors.transparent, | ||||
|       openColor: Theme.of(context).colorScheme.surface, | ||||
|       onTap: () { | ||||
|         if (widget.isClickable) { | ||||
|           AppRouter.instance.pushNamed( | ||||
|             'postDetail', | ||||
|             pathParameters: {'id': item.id.toString()}, | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -293,6 +298,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget { | ||||
|         } | ||||
|  | ||||
|         return Container( | ||||
|           padding: EdgeInsets.only(top: 8), | ||||
|           constraints: const BoxConstraints(maxWidth: 480), | ||||
|           child: Card( | ||||
|             margin: EdgeInsets.zero, | ||||
| @@ -389,8 +395,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget { | ||||
|  | ||||
| class _PostAttachmentWidget extends StatelessWidget { | ||||
|   final Post item; | ||||
|   final EdgeInsets? padding; | ||||
|  | ||||
|   const _PostAttachmentWidget({required this.item}); | ||||
|   const _PostAttachmentWidget({required this.item, required this.padding}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -402,14 +409,22 @@ class _PostAttachmentWidget extends StatelessWidget { | ||||
|  | ||||
|     if (attachments.isEmpty) return const SizedBox.shrink(); | ||||
|  | ||||
|     if (attachments.length == 1) { | ||||
|     if (attachments.length == 1 && !isLargeScreen) { | ||||
|       return AttachmentList( | ||||
|         parentId: item.id.toString(), | ||||
|         attachmentIds: item.preload == null ? attachments : null, | ||||
|         attachments: item.preload?.attachments, | ||||
|         autoload: false, | ||||
|         isFullWidth: true, | ||||
|       ).paddingOnly(top: 4); | ||||
|       ); | ||||
|     } else if (attachments.length == 1) { | ||||
|       return AttachmentList( | ||||
|         parentId: item.id.toString(), | ||||
|         attachmentIds: item.preload == null ? attachments : null, | ||||
|         attachments: item.preload?.attachments, | ||||
|         autoload: false, | ||||
|         isColumn: true, | ||||
|       ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14); | ||||
|     } else if (attachments.length > 1 && | ||||
|         attachments.length % 3 == 0 && | ||||
|         !isLargeScreen) { | ||||
| @@ -419,14 +434,17 @@ class _PostAttachmentWidget extends StatelessWidget { | ||||
|         attachments: item.preload?.attachments, | ||||
|         autoload: false, | ||||
|         isGrid: true, | ||||
|       ).paddingSymmetric(horizontal: 14, vertical: 8); | ||||
|       ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14); | ||||
|     } else { | ||||
|       return AttachmentList( | ||||
|         parentId: item.id.toString(), | ||||
|         attachmentIds: item.preload == null ? attachments : null, | ||||
|         attachments: item.preload?.attachments, | ||||
|         padding: EdgeInsets.symmetric( | ||||
|           horizontal: (padding?.horizontal ?? 0) + 14, | ||||
|         ), | ||||
|         autoload: false, | ||||
|       ).paddingOnly(bottom: 8, top: 4); | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -568,7 +586,7 @@ class _PostFooterWidget extends StatelessWidget { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: widgets, | ||||
|       ).paddingOnly(top: 4); | ||||
|       ).paddingSymmetric(vertical: 4); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -60,8 +60,9 @@ class PostListEntryWidget extends StatelessWidget { | ||||
|   final bool isClickable; | ||||
|   final bool showFeaturedReply; | ||||
|   final Post item; | ||||
|   final Function onUpdate; | ||||
|   final Color? backgroundColor; | ||||
|   final EdgeInsets? padding; | ||||
|   final Function onUpdate; | ||||
|  | ||||
|   const PostListEntryWidget({ | ||||
|     super.key, | ||||
| @@ -70,8 +71,9 @@ class PostListEntryWidget extends StatelessWidget { | ||||
|     required this.isClickable, | ||||
|     required this.showFeaturedReply, | ||||
|     required this.item, | ||||
|     required this.onUpdate, | ||||
|     this.backgroundColor, | ||||
|     this.padding, | ||||
|     required this.onUpdate, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -83,6 +85,7 @@ class PostListEntryWidget extends StatelessWidget { | ||||
|         isShowEmbed: isShowEmbed, | ||||
|         isClickable: isNestedClickable, | ||||
|         showFeaturedReply: showFeaturedReply, | ||||
|         padding: padding, | ||||
|         backgroundColor: backgroundColor, | ||||
|         onComment: () { | ||||
|           AppRouter.instance | ||||
| @@ -129,6 +132,7 @@ class ControlledPostListWidget extends StatelessWidget { | ||||
|   final bool isNestedClickable; | ||||
|   final bool isPinned; | ||||
|   final PagingController<int, Post> controller; | ||||
|   final EdgeInsets? padding; | ||||
|   final Function? onUpdate; | ||||
|  | ||||
|   const ControlledPostListWidget({ | ||||
| @@ -138,6 +142,7 @@ class ControlledPostListWidget extends StatelessWidget { | ||||
|     this.isClickable = true, | ||||
|     this.isNestedClickable = true, | ||||
|     this.isPinned = true, | ||||
|     this.padding, | ||||
|     this.onUpdate, | ||||
|   }); | ||||
|  | ||||
| @@ -156,6 +161,7 @@ class ControlledPostListWidget extends StatelessWidget { | ||||
|             isNestedClickable: isNestedClickable, | ||||
|             isClickable: isClickable, | ||||
|             showFeaturedReply: true, | ||||
|             padding: padding, | ||||
|             item: item, | ||||
|             onUpdate: onUpdate ?? () {}, | ||||
|           ); | ||||
|   | ||||
| @@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart'; | ||||
|  | ||||
| class PostReplyList extends StatefulWidget { | ||||
|   final Post item; | ||||
|   final EdgeInsets? padding; | ||||
|   final Color? backgroundColor; | ||||
|  | ||||
|   const PostReplyList({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.padding, | ||||
|     this.backgroundColor, | ||||
|   }); | ||||
|  | ||||
| @@ -53,7 +55,7 @@ class _PostReplyListState extends State<PostReplyList> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return PostListWidget( | ||||
|       padding: EdgeInsets.symmetric(horizontal: 10), | ||||
|       padding: widget.padding, | ||||
|       isShowEmbed: false, | ||||
|       controller: _pagingController, | ||||
|       backgroundColor: widget.backgroundColor, | ||||
| @@ -93,6 +95,7 @@ class PostReplyListPopup extends StatelessWidget { | ||||
|             slivers: [ | ||||
|               PostReplyList( | ||||
|                 item: item, | ||||
|                 padding: EdgeInsets.symmetric(horizontal: 10), | ||||
|                 backgroundColor: | ||||
|                     Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|               ), | ||||
|   | ||||
							
								
								
									
										12
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -198,14 +198,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.1" | ||||
|   carousel_slider: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: carousel_slider | ||||
|       sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.0.0" | ||||
|   characters: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2278,10 +2270,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" | ||||
|       sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.5.5" | ||||
|     version: "5.6.0" | ||||
|   win32_registry: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -18,7 +18,6 @@ dependencies: | ||||
|   flutter_markdown: ^0.7.1 | ||||
|   flutter_animate: ^4.5.0 | ||||
|   flutter_secure_storage: ^9.2.1 | ||||
|   carousel_slider: ^5.0.0 | ||||
|   url_launcher: ^6.2.6 | ||||
|   infinite_scroll_pagination: ^4.0.0 | ||||
|   image_picker: ^1.1.1 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user