Explore shuffle post

This commit is contained in:
2025-09-04 00:52:02 +08:00
parent 3aece9316c
commit e9f09bbe54
6 changed files with 164 additions and 87 deletions

View File

@@ -637,6 +637,7 @@
"realmJoinSuccess": "Successfully joined the realm.", "realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Realms", "discoverRealms": "Realms",
"discoverPublishers": "Publishers", "discoverPublishers": "Publishers",
"discoverShuffledPost": "Random Posts",
"search": "Search", "search": "Search",
"publisherMembers": "Collaborators", "publisherMembers": "Collaborators",
"developerHub": "Developer Hub", "developerHub": "Developer Hub",

View File

@@ -7,7 +7,7 @@ part of 'room_detail.dart';
// ************************************************************************** // **************************************************************************
String _$totalMessagesCountHash() => String _$totalMessagesCountHash() =>
r'a15c03461f25c2d4d39c0926509bf626ae2550a6'; r'd55f1507aba2acdce5e468c1c2e15dba7640c571';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -399,6 +399,69 @@ class _DiscoveryActivityItem extends StatelessWidget {
final items = data['items'] as List; final items = data['items'] as List;
final type = items.firstOrNull?['type'] ?? 'unknown'; final type = items.firstOrNull?['type'] ?? 'unknown';
var flexWeights = isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1];
if (type == 'post') flexWeights = <int>[3, 2];
final height = type == 'post' ? 280.0 : 180.0;
final contentWidget = switch (type) {
'post' => ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: items.length,
separatorBuilder: (context, index) => const Gap(12),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
itemBuilder: (context, index) {
final item = items[index];
return Container(
width: 320,
decoration: BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SingleChildScrollView(
child: PostActionableItem(
item: SnPost.fromJson(item['data']),
isCompact: true,
),
),
),
);
},
),
_ => CarouselView.weighted(
flexWeights: flexWeights,
consumeMaxWeight: false,
enableSplash: false,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
itemSnapping: false,
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),
_ => const Placeholder(),
},
],
),
};
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column( child: Column(
@@ -407,13 +470,20 @@ class _DiscoveryActivityItem extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Icon(Symbols.explore, size: 19), Icon(switch (type) {
'realm' => Symbols.public,
'publisher' => Symbols.account_circle,
'article' => Symbols.auto_stories,
'post' => Symbols.shuffle,
_ => Symbols.explore,
}, size: 19),
const Gap(8), const Gap(8),
Text( Text(
(switch (type) { (switch (type) {
'realm' => 'discoverRealms', 'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers', 'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles', 'article' => 'discoverWebArticles',
'post' => 'discoverShuffledPost',
_ => 'unknown', _ => 'unknown',
}).tr(), }).tr(),
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
@@ -421,37 +491,8 @@ class _DiscoveryActivityItem extends StatelessWidget {
], ],
).padding(horizontal: 20, top: 8, bottom: 4), ).padding(horizontal: 20, top: 8, bottom: 4),
SizedBox( SizedBox(
height: 180, height: height,
child: ConstrainedBox( child: contentWidget,
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView.weighted(
flexWeights:
isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1],
consumeMaxWeight: false,
enableSplash: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),
_ => Placeholder(),
},
],
),
),
).padding(bottom: 8, horizontal: 8), ).padding(bottom: 8, horizontal: 8),
], ],
), ),
@@ -569,7 +610,8 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor, if (cursor != null) 'cursor': cursor,
'take': take, 'take': take,
if (filter != null) 'filter': filter, if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers,articles', if (kDebugMode)
'debugInclude': 'realms,publishers,articles,shuffledPosts',
}; };
final response = await client.get( final response = await client.get(
@@ -584,12 +626,13 @@ class ActivityListNotifier extends _$ActivityListNotifier
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty'; final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
final nextCursor = final nextCursor =
items items.isNotEmpty
.map((x) => x.createdAt) ? items
.lastOrNull .map((x) => x.createdAt)
?.toUtc() .reduce((a, b) => a.isAfter(b) ? a : b)
.toIso8601String() .toUtc()
.toString(); .toIso8601String()
: null;
return CursorPagingData( return CursorPagingData(
items: items, items: items,

View File

@@ -7,7 +7,7 @@ part of 'explore.dart';
// ************************************************************************** // **************************************************************************
String _$activityListNotifierHash() => String _$activityListNotifierHash() =>
r'b75fd5c08d5f84ca433e16b7387d317ea72b91c9'; r'a4968856ac34b59d47cfd4a7cbb39289aef2a1b1';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -36,6 +36,7 @@ class PostActionableItem extends HookConsumerWidget {
final bool isShowReference; final bool isShowReference;
final bool isEmbedReply; final bool isEmbedReply;
final bool isEmbedOpenable; final bool isEmbedOpenable;
final bool isCompact;
final double? borderRadius; final double? borderRadius;
final VoidCallback? onRefresh; final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
@@ -48,6 +49,7 @@ class PostActionableItem extends HookConsumerWidget {
this.isShowReference = true, this.isShowReference = true,
this.isEmbedReply = true, this.isEmbedReply = true,
this.isEmbedOpenable = false, this.isEmbedOpenable = false,
this.isCompact = false,
this.borderRadius, this.borderRadius,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
@@ -76,6 +78,7 @@ class PostActionableItem extends HookConsumerWidget {
isEmbedReply: isEmbedReply, isEmbedReply: isEmbedReply,
isEmbedOpenable: isEmbedOpenable, isEmbedOpenable: isEmbedOpenable,
isTextSelectable: false, isTextSelectable: false,
isCompact: isCompact,
onRefresh: onRefresh, onRefresh: onRefresh,
onUpdate: onUpdate, onUpdate: onUpdate,
onOpen: onOpen, onOpen: onOpen,
@@ -298,6 +301,7 @@ class PostItem extends HookConsumerWidget {
final bool isEmbedOpenable; final bool isEmbedOpenable;
final bool isTextSelectable; final bool isTextSelectable;
final bool isTranslatable; final bool isTranslatable;
final bool isCompact;
final VoidCallback? onRefresh; final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
final VoidCallback? onOpen; final VoidCallback? onOpen;
@@ -311,6 +315,7 @@ class PostItem extends HookConsumerWidget {
this.isEmbedOpenable = false, this.isEmbedOpenable = false,
this.isTextSelectable = true, this.isTextSelectable = true,
this.isTranslatable = true, this.isTranslatable = true,
this.isCompact = false,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
this.onOpen, this.onOpen,
@@ -465,54 +470,64 @@ class PostItem extends HookConsumerWidget {
PostHeader( PostHeader(
item: item, item: item,
isFullPost: isFullPost, isFullPost: isFullPost,
isCompact: isCompact,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
trailing: IconButton( trailing:
icon: isCompact
mostReaction == null ? null
? const Icon(Symbols.add_reaction) : IconButton(
: Badge( icon:
label: Center( mostReaction == null
child: Text( ? const Icon(Symbols.add_reaction)
'x${item.reactionsCount[mostReaction]}', : Badge(
style: const TextStyle(fontSize: 11), label: Center(
textAlign: TextAlign.center, child: Text(
), 'x${item.reactionsCount[mostReaction]}',
), style: const TextStyle(fontSize: 11),
offset: const Offset(4, 20), textAlign: TextAlign.center,
backgroundColor: Theme.of( ),
context, ),
).colorScheme.primary.withOpacity(0.75), offset: const Offset(4, 20),
textColor: Theme.of(context).colorScheme.onPrimary, backgroundColor: Theme.of(
child: Text( context,
kReactionTemplates[mostReaction]?.icon ?? '', ).colorScheme.primary.withOpacity(0.75),
style: const TextStyle(fontSize: 20), textColor:
Theme.of(context).colorScheme.onPrimary,
child: Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
),
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
(item.reactionsMade[mostReaction] ?? false)
? Theme.of(
context,
).colorScheme.primary.withOpacity(0.5)
: null,
), ),
), ),
style: ButtonStyle( onPressed: () {
backgroundColor: WidgetStatePropertyAll( showModalBottomSheet(
(item.reactionsMade[mostReaction] ?? false) context: context,
? Theme.of(context).colorScheme.primary.withOpacity(0.5) useRootNavigator: true,
: null, builder: (BuildContext context) {
), return _PostReactionSheet(
), reactionsCount: item.reactionsCount,
onPressed: () { reactionsMade: item.reactionsMade,
showModalBottomSheet( onReact: (symbol, attitude) {
context: context, reactPost(symbol, attitude);
useRootNavigator: true, },
builder: (BuildContext context) { );
return _PostReactionSheet( },
reactionsCount: item.reactionsCount, );
reactionsMade: item.reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
}, },
); padding: EdgeInsets.zero,
}, visualDensity: const VisualDensity(
); horizontal: -3,
}, vertical: -3,
padding: EdgeInsets.zero, ),
visualDensity: const VisualDensity(horizontal: -3, vertical: -3), ),
),
), ),
PostBody( PostBody(
item: item, item: item,

View File

@@ -532,6 +532,7 @@ class PostHeader extends StatelessWidget {
final bool isInteractive; final bool isInteractive;
final EdgeInsets renderingPadding; final EdgeInsets renderingPadding;
final bool isRelativeTime; final bool isRelativeTime;
final bool isCompact;
const PostHeader({ const PostHeader({
super.key, super.key,
@@ -541,6 +542,7 @@ class PostHeader extends StatelessWidget {
this.isInteractive = true, this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero, this.renderingPadding = EdgeInsets.zero,
this.isRelativeTime = true, this.isRelativeTime = true,
this.isCompact = false,
}); });
@override @override
@@ -584,11 +586,27 @@ class PostHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4, spacing: 4,
children: [ children: [
Text(item.publisher.nick).bold(), Flexible(
child:
Text(
item.publisher.nick,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).bold(),
),
if (item.publisher.verification != null) if (item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!), VerificationMark(mark: item.publisher.verification!),
if (item.realm == null) if (item.realm == null)
Text('@${item.publisher.name}').fontSize(11) Flexible(
child:
isCompact
? const SizedBox.shrink()
: Text(
'@${item.publisher.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(11),
)
else else
...([ ...([
const Icon(Symbols.arrow_right, size: 14), const Icon(Symbols.arrow_right, size: 14),