💄 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/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.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/extended_refresh_indicator.dart';
import 'package:island/widgets/post/post_award_sheet.dart'; import 'package:island/widgets/post/post_award_sheet.dart';
import 'package:island/widgets/post/post_item.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/post_quick_reply.dart';
import 'package:island/widgets/post/compose_sheet.dart'; import 'package:island/widgets/post/compose_sheet.dart';
import 'package:island/widgets/post/post_replies.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/widgets/response.dart';
import 'package:island/utils/share_utils.dart'; import 'package:island/utils/share_utils.dart';
import 'package:island/widgets/safety/abuse_report_helper.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 { class PostActionButtons extends HookConsumerWidget {
final SnPost post; final SnPost post;
final EdgeInsets renderingPadding; 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 { class PostDetailScreen extends HookConsumerWidget {
final String id; final String id;
const PostDetailScreen({super.key, required this.id}); const PostDetailScreen({super.key, required this.id});
@@ -452,6 +541,8 @@ class PostDetailScreen extends HookConsumerWidget {
), ),
body: postState.when( body: postState.when(
data: (post) { data: (post) {
final isMediaPostLayout = isWideScreen(context) && _isMediaPost(post);
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -460,64 +551,78 @@ class PostDetailScreen extends HookConsumerWidget {
ref.invalidate(postProvider(id)); ref.invalidate(postProvider(id));
ref.read(postRepliesProvider(id).notifier).refresh(); ref.read(postRepliesProvider(id).notifier).refresh();
}, },
child: CustomScrollView( child: isMediaPostLayout
physics: const AlwaysScrollableScrollPhysics(), ? _PostDetailLargeScreenLayout(
slivers: [ post: post!,
SliverToBoxAdapter( ref: ref,
child: Center( postId: id,
child: ConstrainedBox( onUpdate: (newItem) {
constraints: BoxConstraints(maxWidth: 800), ref
child: PostItem( .read(postStateProvider(id).notifier)
item: post!, .updatePost(newItem);
isFullPost: true, },
isEmbedReply: false, onRefresh: () {
textScale: post.type == 1 ? 1.2 : 1.1, ref.invalidate(postProvider(id));
onUpdate: (newItem) { ref.read(postRepliesProvider(id).notifier).refresh();
// Update the local state with the new post data },
ref )
.read(postStateProvider(id).notifier) : CustomScrollView(
.updatePost(newItem); physics: const AlwaysScrollableScrollPhysics(),
}, slivers: [
), SliverToBoxAdapter(
), child: Center(
), child: ConstrainedBox(
), constraints: BoxConstraints(maxWidth: 800),
SliverToBoxAdapter( child: PostItem(
child: Center( item: post!,
child: ConstrainedBox( isFullPost: true,
constraints: BoxConstraints(maxWidth: 800), isEmbedReply: false,
child: PostActionButtons( textScale: post.type == 1 ? 1.2 : 1.1,
post: post, onUpdate: (newItem) {
renderingPadding: const EdgeInsets.symmetric( ref
horizontal: 8, .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( Positioned(
bottom: 16 + MediaQuery.of(context).padding.bottom, bottom: 16 + MediaQuery.of(context).padding.bottom,
left: 16, left: 16,
right: 16, right: 16,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 660), constraints: BoxConstraints(maxWidth: 800),
child: postState.when( child: postState.when(
data: (post) => PostQuickReply( data: (post) => PostQuickReply(
parent: post!, parent: post!,

View File

@@ -167,7 +167,9 @@ class _PostsSearchTab extends HookConsumerWidget {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
), ),
child: const PostItemSkeleton(), child: const PostItemSkeleton(
maxWidth: double.infinity,
),
), ),
itemBuilder: (context, index, post) { itemBuilder: (context, index, post) {
return Card( return Card(
@@ -320,7 +322,7 @@ class _PostsSearchTab extends HookConsumerWidget {
isRefreshable: false, isRefreshable: false,
footerSkeletonChild: Padding( footerSkeletonChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: const PostItemSkeleton(), child: const PostItemSkeleton(maxWidth: double.infinity),
), ),
itemBuilder: (context, index, post) { itemBuilder: (context, index, post) {
return Center( return Center(

View File

@@ -67,6 +67,7 @@ class PostActionableItem extends HookConsumerWidget {
final bool isEmbedReply; final bool isEmbedReply;
final bool isEmbedOpenable; final bool isEmbedOpenable;
final bool isCompact; final bool isCompact;
final bool hideAttachments;
final double? borderRadius; final double? borderRadius;
final VoidCallback? onRefresh; final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
@@ -80,6 +81,7 @@ class PostActionableItem extends HookConsumerWidget {
this.isEmbedReply = true, this.isEmbedReply = true,
this.isEmbedOpenable = false, this.isEmbedOpenable = false,
this.isCompact = false, this.isCompact = false,
this.hideAttachments = false,
this.borderRadius, this.borderRadius,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
@@ -110,6 +112,7 @@ class PostActionableItem extends HookConsumerWidget {
isEmbedOpenable: isEmbedOpenable, isEmbedOpenable: isEmbedOpenable,
isTextSelectable: false, isTextSelectable: false,
isCompact: isCompact, isCompact: isCompact,
hideAttachments: hideAttachments,
onRefresh: onRefresh, onRefresh: onRefresh,
onUpdate: onUpdate, onUpdate: onUpdate,
onOpen: onOpen, onOpen: onOpen,
@@ -308,6 +311,7 @@ class PostItem extends HookConsumerWidget {
final bool isTextSelectable; final bool isTextSelectable;
final bool isTranslatable; final bool isTranslatable;
final bool isCompact; final bool isCompact;
final bool hideAttachments;
final double? textScale; final double? textScale;
final VoidCallback? onRefresh; final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
@@ -323,6 +327,7 @@ class PostItem extends HookConsumerWidget {
this.isTextSelectable = true, this.isTextSelectable = true,
this.isTranslatable = true, this.isTranslatable = true,
this.isCompact = false, this.isCompact = false,
this.hideAttachments = false,
this.textScale, this.textScale,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
@@ -569,6 +574,7 @@ class PostItem extends HookConsumerWidget {
isTextSelectable: isTextSelectable, isTextSelectable: isTextSelectable,
translationSection: translationSection, translationSection: translationSection,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
hideAttachments: hideAttachments,
), ),
if (item.embedView != null) if (item.embedView != null)
EmbedViewRenderer( EmbedViewRenderer(

View File

@@ -69,7 +69,7 @@ class SliverPostList extends HookConsumerWidget {
isSliver: true, isSliver: true,
footerSkeletonChild: Padding( footerSkeletonChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: const PostItemSkeleton(), child: PostItemSkeleton(maxWidth: maxWidth ?? double.infinity),
), ),
itemBuilder: (context, index, post) { itemBuilder: (context, index, post) {
if (maxWidth != null) { if (maxWidth != null) {

View File

@@ -50,7 +50,7 @@ class PostRepliesList extends HookConsumerWidget {
final skeletonItem = Padding( final skeletonItem = Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: const PostItemSkeleton(), child: PostItemSkeleton(maxWidth: maxWidth ?? double.infinity),
); );
return PaginationList( return PaginationList(

View File

@@ -870,6 +870,7 @@ class PostBody extends ConsumerWidget {
final EdgeInsets renderingPadding; final EdgeInsets renderingPadding;
final bool isRelativeTime; final bool isRelativeTime;
final bool hideOverlay; final bool hideOverlay;
final bool hideAttachments;
final double? textScale; final double? textScale;
const PostBody({ const PostBody({
@@ -882,6 +883,7 @@ class PostBody extends ConsumerWidget {
this.renderingPadding = EdgeInsets.zero, this.renderingPadding = EdgeInsets.zero,
this.isRelativeTime = true, this.isRelativeTime = true,
this.hideOverlay = false, this.hideOverlay = false,
this.hideAttachments = false,
this.textScale, this.textScale,
}); });
@@ -1140,7 +1142,7 @@ class PostBody extends ConsumerWidget {
right: renderingPadding.horizontal, right: renderingPadding.horizontal,
), ),
), ),
if (item.attachments.isNotEmpty && item.type != 1) if (item.attachments.isNotEmpty && item.type != 1 && !hideAttachments)
CloudFileList( CloudFileList(
files: item.attachments, files: item.attachments,
isColumn: !isInteractive, isColumn: !isInteractive,