💄 New media post layout

This commit is contained in:
2026-01-07 23:30:48 +08:00
parent 38fc4e969f
commit 9cba568e47
6 changed files with 169 additions and 54 deletions

View File

@@ -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<AsyncValue<SnPost?>> {
}
}
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!,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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,