From 9cba568e47aac6eafb394a51f771ed1a56b53237 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 7 Jan 2026 23:30:48 +0800 Subject: [PATCH] :lipstick: New media post layout --- lib/screens/posts/post_detail.dart | 203 ++++++++++++++++++++++------- lib/screens/search.dart | 6 +- lib/widgets/post/post_item.dart | 6 + lib/widgets/post/post_list.dart | 2 +- lib/widgets/post/post_replies.dart | 2 +- lib/widgets/post/post_shared.dart | 4 +- 6 files changed, 169 insertions(+), 54 deletions(-) diff --git a/lib/screens/posts/post_detail.dart b/lib/screens/posts/post_detail.dart index a9358e9e..f1c7ac6f 100644 --- a/lib/screens/posts/post_detail.dart +++ b/lib/screens/posts/post_detail.dart @@ -9,8 +9,10 @@ import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/screens/posts/compose.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/post/post_award_sheet.dart'; import 'package:island/widgets/post/post_item.dart'; @@ -19,6 +21,7 @@ import 'package:island/widgets/post/post_pin_sheet.dart'; import 'package:island/widgets/post/post_quick_reply.dart'; import 'package:island/widgets/post/compose_sheet.dart'; import 'package:island/widgets/post/post_replies.dart'; +import 'package:island/widgets/post/post_shared.dart'; import 'package:island/widgets/response.dart'; import 'package:island/utils/share_utils.dart'; import 'package:island/widgets/safety/abuse_report_helper.dart'; @@ -62,6 +65,10 @@ class PostState extends Notifier> { } } +bool _isMediaPost(SnPost? post) { + return post != null && post.type == 0 && post.attachments.isNotEmpty; +} + class PostActionButtons extends HookConsumerWidget { final SnPost post; final EdgeInsets renderingPadding; @@ -435,6 +442,88 @@ class PostActionButtons extends HookConsumerWidget { } } +class _PostDetailLargeScreenLayout extends StatelessWidget { + final SnPost post; + final WidgetRef ref; + final String postId; + final Function(SnPost) onUpdate; + final VoidCallback onRefresh; + + const _PostDetailLargeScreenLayout({ + required this.post, + required this.ref, + required this.postId, + required this.onUpdate, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + flex: 3, + child: Material( + color: Theme.of(context).cardTheme.color, + elevation: 8, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: CloudFileList( + files: post.attachments, + disableConstraint: true, + padding: EdgeInsets.zero, + ), + ), + ), + ), + ), + Expanded( + flex: 2, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PostHeader( + item: post, + isFullPost: true, + isCompact: false, + renderingPadding: EdgeInsets.zero, + ), + const Gap(8), + PostBody( + item: post, + isFullPost: true, + isTextSelectable: true, + renderingPadding: EdgeInsets.zero, + hideAttachments: true, + textScale: post.type == 1 ? 1.2 : 1.1, + ), + const Gap(12), + PostActionButtons( + post: post, + renderingPadding: EdgeInsets.zero, + onRefresh: onRefresh, + onUpdate: onUpdate, + ), + ], + ), + ), + ), + PostRepliesList(postId: postId, maxWidth: 800), + SliverGap(MediaQuery.of(context).padding.bottom + 80), + ], + ), + ), + ], + ); + } +} + class PostDetailScreen extends HookConsumerWidget { final String id; const PostDetailScreen({super.key, required this.id}); @@ -452,6 +541,8 @@ class PostDetailScreen extends HookConsumerWidget { ), body: postState.when( data: (post) { + final isMediaPostLayout = isWideScreen(context) && _isMediaPost(post); + return Stack( fit: StackFit.expand, children: [ @@ -460,64 +551,78 @@ class PostDetailScreen extends HookConsumerWidget { ref.invalidate(postProvider(id)); ref.read(postRepliesProvider(id).notifier).refresh(); }, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 800), - child: PostItem( - item: post!, - isFullPost: true, - isEmbedReply: false, - textScale: post.type == 1 ? 1.2 : 1.1, - onUpdate: (newItem) { - // Update the local state with the new post data - ref - .read(postStateProvider(id).notifier) - .updatePost(newItem); - }, - ), - ), - ), - ), - SliverToBoxAdapter( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 800), - child: PostActionButtons( - post: post, - renderingPadding: const EdgeInsets.symmetric( - horizontal: 8, + child: isMediaPostLayout + ? _PostDetailLargeScreenLayout( + post: post!, + ref: ref, + postId: id, + onUpdate: (newItem) { + ref + .read(postStateProvider(id).notifier) + .updatePost(newItem); + }, + onRefresh: () { + ref.invalidate(postProvider(id)); + ref.read(postRepliesProvider(id).notifier).refresh(); + }, + ) + : CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 800), + child: PostItem( + item: post!, + isFullPost: true, + isEmbedReply: false, + textScale: post.type == 1 ? 1.2 : 1.1, + onUpdate: (newItem) { + ref + .read(postStateProvider(id).notifier) + .updatePost(newItem); + }, + ), + ), ), - onRefresh: () { - ref.invalidate(postProvider(id)); - ref - .read(postRepliesProvider(id).notifier) - .refresh(); - }, - onUpdate: (newItem) { - ref - .read(postStateProvider(id).notifier) - .updatePost(newItem); - }, ), - ), + SliverToBoxAdapter( + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 800), + child: PostActionButtons( + post: post, + renderingPadding: const EdgeInsets.symmetric( + horizontal: 8, + ), + onRefresh: () { + ref.invalidate(postProvider(id)); + ref + .read(postRepliesProvider(id).notifier) + .refresh(); + }, + onUpdate: (newItem) { + ref + .read(postStateProvider(id).notifier) + .updatePost(newItem); + }, + ), + ), + ), + ), + PostRepliesList(postId: id, maxWidth: 800), + SliverGap(MediaQuery.of(context).padding.bottom + 80), + ], ), - ), - PostRepliesList(postId: id, maxWidth: 800), - SliverGap(MediaQuery.of(context).padding.bottom + 80), - ], - ), ), - if (user.value != null) + if (user.value != null && !isMediaPostLayout) Positioned( bottom: 16 + MediaQuery.of(context).padding.bottom, left: 16, right: 16, child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 660), + constraints: BoxConstraints(maxWidth: 800), child: postState.when( data: (post) => PostQuickReply( parent: post!, diff --git a/lib/screens/search.dart b/lib/screens/search.dart index 2855402c..3c52dcbb 100644 --- a/lib/screens/search.dart +++ b/lib/screens/search.dart @@ -167,7 +167,9 @@ class _PostsSearchTab extends HookConsumerWidget { padding: const EdgeInsets.symmetric( horizontal: 8, ), - child: const PostItemSkeleton(), + child: const PostItemSkeleton( + maxWidth: double.infinity, + ), ), itemBuilder: (context, index, post) { return Card( @@ -320,7 +322,7 @@ class _PostsSearchTab extends HookConsumerWidget { isRefreshable: false, footerSkeletonChild: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: const PostItemSkeleton(), + child: const PostItemSkeleton(maxWidth: double.infinity), ), itemBuilder: (context, index, post) { return Center( diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index e15e9701..99ad3e1d 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -67,6 +67,7 @@ class PostActionableItem extends HookConsumerWidget { final bool isEmbedReply; final bool isEmbedOpenable; final bool isCompact; + final bool hideAttachments; final double? borderRadius; final VoidCallback? onRefresh; final Function(SnPost)? onUpdate; @@ -80,6 +81,7 @@ class PostActionableItem extends HookConsumerWidget { this.isEmbedReply = true, this.isEmbedOpenable = false, this.isCompact = false, + this.hideAttachments = false, this.borderRadius, this.onRefresh, this.onUpdate, @@ -110,6 +112,7 @@ class PostActionableItem extends HookConsumerWidget { isEmbedOpenable: isEmbedOpenable, isTextSelectable: false, isCompact: isCompact, + hideAttachments: hideAttachments, onRefresh: onRefresh, onUpdate: onUpdate, onOpen: onOpen, @@ -308,6 +311,7 @@ class PostItem extends HookConsumerWidget { final bool isTextSelectable; final bool isTranslatable; final bool isCompact; + final bool hideAttachments; final double? textScale; final VoidCallback? onRefresh; final Function(SnPost)? onUpdate; @@ -323,6 +327,7 @@ class PostItem extends HookConsumerWidget { this.isTextSelectable = true, this.isTranslatable = true, this.isCompact = false, + this.hideAttachments = false, this.textScale, this.onRefresh, this.onUpdate, @@ -569,6 +574,7 @@ class PostItem extends HookConsumerWidget { isTextSelectable: isTextSelectable, translationSection: translationSection, renderingPadding: renderingPadding, + hideAttachments: hideAttachments, ), if (item.embedView != null) EmbedViewRenderer( diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index a259a143..002d81e6 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -69,7 +69,7 @@ class SliverPostList extends HookConsumerWidget { isSliver: true, footerSkeletonChild: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: const PostItemSkeleton(), + child: PostItemSkeleton(maxWidth: maxWidth ?? double.infinity), ), itemBuilder: (context, index, post) { if (maxWidth != null) { diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index 139dc4bb..4473427a 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -50,7 +50,7 @@ class PostRepliesList extends HookConsumerWidget { final skeletonItem = Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: const PostItemSkeleton(), + child: PostItemSkeleton(maxWidth: maxWidth ?? double.infinity), ); return PaginationList( diff --git a/lib/widgets/post/post_shared.dart b/lib/widgets/post/post_shared.dart index 60d06268..2f23c280 100644 --- a/lib/widgets/post/post_shared.dart +++ b/lib/widgets/post/post_shared.dart @@ -870,6 +870,7 @@ class PostBody extends ConsumerWidget { final EdgeInsets renderingPadding; final bool isRelativeTime; final bool hideOverlay; + final bool hideAttachments; final double? textScale; const PostBody({ @@ -882,6 +883,7 @@ class PostBody extends ConsumerWidget { this.renderingPadding = EdgeInsets.zero, this.isRelativeTime = true, this.hideOverlay = false, + this.hideAttachments = false, this.textScale, }); @@ -1140,7 +1142,7 @@ class PostBody extends ConsumerWidget { right: renderingPadding.horizontal, ), ), - if (item.attachments.isNotEmpty && item.type != 1) + if (item.attachments.isNotEmpty && item.type != 1 && !hideAttachments) CloudFileList( files: item.attachments, isColumn: !isInteractive,