Realm page redesgiened

This commit is contained in:
2025-08-17 19:44:43 +08:00
parent 2bdf7029e9
commit 8370da4fe3
4 changed files with 477 additions and 304 deletions

View File

@@ -4,6 +4,8 @@ import 'package:island/screens/chat/chat.dart';
import 'package:flutter/material.dart';
import 'package:island/models/chat.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -78,39 +80,131 @@ class RealmDetailScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0),
);
final realmIdentity = ref.watch(realmIdentityProvider(slug));
final realmChatRooms = ref.watch(realmChatRoomsProvider(slug));
Widget realmDescriptionWidget(SnRealm realm) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
collapsedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
title: const Text('description').tr(),
initiallyExpanded:
realmIdentity.hasValue && realmIdentity.value == null,
tilePadding: EdgeInsets.only(left: 24, right: 20),
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
realm.description,
style: const TextStyle(fontSize: 16),
).padding(horizontal: 20, bottom: 16, top: 8),
],
),
),
);
Widget realmActionWidget(SnRealm realm) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/realms/$slug/members/me');
ref.invalidate(realmIdentityProvider(slug));
ref.invalidate(realmsJoinedProvider);
showSnackBar('realmJoinSuccess'.tr());
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.add),
label: const Text('realmJoin').tr(),
).padding(all: 16),
);
Widget realmChatRoomListWidget(SnRealm realm) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'chatTabGroup',
).tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
realmChatRooms.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
data: (rooms) {
if (rooms.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return Column(
children: [
for (final room in rooms)
ChatRoomListTile(
room: room,
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
},
),
],
);
},
),
],
),
);
return AppScaffold(
isNoBackground: false,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
appBar:
isWideScreen(context)
? realmState.when(
data:
(realm) => CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
(realm) => AppBar(
foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [iconShadow],
),
flexibleSpace: FlexibleSpaceBar(
background:
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
realm!.background?.id != null
? CloudImageWidget(fileId: realm.background!.id)
? CloudImageWidget(
fileId: realm.background!.id,
)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
realm.name,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [iconShadow],
),
),
background: Container(),
),
],
),
actions: [
IconButton(
@@ -125,106 +219,144 @@ class RealmDetailScreen extends HookConsumerWidget {
);
},
),
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
_RealmActionMenu(
realmSlug: slug,
iconShadow: iconShadow,
),
const Gap(8),
],
),
SliverToBoxAdapter(
child: ref
.watch(realmIdentityProvider(slug))
.when(
error: (_, _) => AppBar(leading: PageBackButton()),
loading: () => AppBar(leading: PageBackButton()),
)
: null,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
data:
(realm) =>
isWideScreen(context)
? Row(
children: [
Flexible(
flex: 3,
child: CustomScrollView(
slivers: [SliverPostList(realm: slug)],
),
),
Flexible(
flex: 2,
child: Column(
children: [
realmIdentity.when(
loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
data:
(identity) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ExpansionTile(
title: const Text('description').tr(),
initiallyExpanded: identity == null,
tilePadding: EdgeInsets.symmetric(
horizontal: 20,
),
expandedCrossAxisAlignment:
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
Text(
realm.description,
style: const TextStyle(fontSize: 16),
).padding(
horizontal: 20,
bottom: 16,
top: 8,
realmDescriptionWidget(realm!),
if (identity == null &&
realm.isCommunity)
realmActionWidget(realm)
else
const SizedBox.shrink(),
],
),
),
realmChatRoomListWidget(realm!),
],
),
),
],
).padding(horizontal: 8, top: 8)
: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [iconShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
realm!.background?.id != null
? CloudImageWidget(
fileId: realm.background!.id,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
realm.name,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [iconShadow],
),
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
if (identity == null && realm.isCommunity)
FilledButton.tonalIcon(
onPressed: () async {
try {
final apiClient = ref.read(
apiClientProvider,
actions: [
IconButton(
icon: Icon(Icons.people, shadows: [iconShadow]),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _RealmMemberListSheet(
realmSlug: slug,
),
);
await apiClient.post(
'/sphere/realms/$slug/members/me',
);
ref.invalidate(
realmIdentityProvider(slug),
);
ref.invalidate(realmsJoinedProvider);
showSnackBar('realmJoinSuccess'.tr());
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.add),
label: const Text('realmJoin').tr(),
).padding(horizontal: 16, vertical: 16)
),
_RealmActionMenu(
realmSlug: slug,
iconShadow: iconShadow,
),
const Gap(8),
],
),
SliverGap(4),
SliverToBoxAdapter(
child: realmIdentity.when(
loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
data:
(identity) => Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
realmDescriptionWidget(realm),
if (identity == null && realm.isCommunity)
realmActionWidget(realm)
else
const SizedBox.shrink(),
],
),
),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
Consumer(
builder: (context, ref, _) {
final chatRooms = ref.watch(realmChatRoomsProvider(slug));
return chatRooms.when(
loading:
() => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
),
error:
(error, _) => SliverToBoxAdapter(
child: Center(child: Text('Error: $error')),
),
data: (rooms) {
if (rooms.isEmpty) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate((
context,
index,
) {
return ChatRoomListTile(
room: rooms[index],
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': rooms[index].id},
);
},
);
}, childCount: rooms.length),
);
},
);
},
SliverToBoxAdapter(
child: realmChatRoomListWidget(realm),
),
SliverPostList(realm: slug),
],
),
),
@@ -508,13 +640,8 @@ class _RealmMemberListSheet extends HookConsumerWidget {
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
Widget _buildMemberListHeader() {
return Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
@@ -547,9 +674,11 @@ class _RealmMemberListSheet extends HookConsumerWidget {
),
],
),
),
const Divider(height: 1),
Expanded(
);
}
Widget _buildMemberListContent() {
return Expanded(
child: PagingHelperView(
provider: memberListProvider,
futureRefreshable: memberListProvider.future,
@@ -624,9 +753,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/sphere/realms/$realmSlug/members/${member.accountId}',
);
@@ -647,7 +774,18 @@ class _RealmMemberListSheet extends HookConsumerWidget {
);
},
),
);
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
_buildMemberListHeader(),
const Divider(height: 1),
_buildMemberListContent(),
],
),
);

View File

@@ -15,8 +15,9 @@ class PostListNotifier extends _$PostListNotifier
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPost>> build(
String? pubName, {
Future<CursorPagingData<SnPost>> build({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
@@ -33,6 +34,7 @@ class PostListNotifier extends _$PostListNotifier
'offset': offset,
'take': _pageSize,
if (pubName != null) 'pub': pubName,
if (realm != null) 'realm': realm,
if (type != null) 'type': type,
if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories,
@@ -68,6 +70,7 @@ enum PostItemType {
class SliverPostList extends HookConsumerWidget {
final String? pubName;
final String? realm;
final int? type;
final List<String>? categories;
final List<String>? tags;
@@ -81,6 +84,7 @@ class SliverPostList extends HookConsumerWidget {
const SliverPostList({
super.key,
this.pubName,
this.realm,
this.type,
this.categories,
this.tags,
@@ -96,21 +100,24 @@ class SliverPostList extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: postListNotifierProvider(
pubName,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
),
futureRefreshable:
postListNotifierProvider(
pubName,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
).future,
notifierRefreshable:
postListNotifierProvider(
pubName,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'2ca4f3cfbbcd04f3cc32e7f7bd511a5811042829';
String _$postListNotifierHash() => r'9784b282b3ee14b7109e263c5841a082cf0be78e';
/// Copied from Dart SDK
class _SystemHash {
@@ -32,12 +32,14 @@ class _SystemHash {
abstract class _$PostListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String? pubName;
late final String? realm;
late final int? type;
late final List<String>? categories;
late final List<String>? tags;
FutureOr<CursorPagingData<SnPost>> build(
String? pubName, {
FutureOr<CursorPagingData<SnPost>> build({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
@@ -55,14 +57,16 @@ class PostListNotifierFamily
const PostListNotifierFamily();
/// See also [PostListNotifier].
PostListNotifierProvider call(
String? pubName, {
PostListNotifierProvider call({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
}) {
return PostListNotifierProvider(
pubName,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
@@ -74,7 +78,8 @@ class PostListNotifierFamily
covariant PostListNotifierProvider provider,
) {
return call(
provider.pubName,
pubName: provider.pubName,
realm: provider.realm,
type: provider.type,
categories: provider.categories,
tags: provider.tags,
@@ -104,8 +109,9 @@ class PostListNotifierProvider
CursorPagingData<SnPost>
> {
/// See also [PostListNotifier].
PostListNotifierProvider(
String? pubName, {
PostListNotifierProvider({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
@@ -113,6 +119,7 @@ class PostListNotifierProvider
() =>
PostListNotifier()
..pubName = pubName
..realm = realm
..type = type
..categories = categories
..tags = tags,
@@ -126,6 +133,7 @@ class PostListNotifierProvider
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
@@ -139,12 +147,14 @@ class PostListNotifierProvider
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
required this.realm,
required this.type,
required this.categories,
required this.tags,
}) : super.internal();
final String? pubName;
final String? realm;
final int? type;
final List<String>? categories;
final List<String>? tags;
@@ -154,7 +164,8 @@ class PostListNotifierProvider
covariant PostListNotifier notifier,
) {
return notifier.build(
pubName,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
@@ -169,6 +180,7 @@ class PostListNotifierProvider
() =>
create()
..pubName = pubName
..realm = realm
..type = type
..categories = categories
..tags = tags,
@@ -178,6 +190,7 @@ class PostListNotifierProvider
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
@@ -198,6 +211,7 @@ class PostListNotifierProvider
bool operator ==(Object other) {
return other is PostListNotifierProvider &&
other.pubName == pubName &&
other.realm == realm &&
other.type == type &&
other.categories == categories &&
other.tags == tags;
@@ -207,6 +221,7 @@ class PostListNotifierProvider
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, realm.hashCode);
hash = _SystemHash.combine(hash, type.hashCode);
hash = _SystemHash.combine(hash, categories.hashCode);
hash = _SystemHash.combine(hash, tags.hashCode);
@@ -222,6 +237,9 @@ mixin PostListNotifierRef
/// The parameter `pubName` of this provider.
String? get pubName;
/// The parameter `realm` of this provider.
String? get realm;
/// The parameter `type` of this provider.
int? get type;
@@ -244,6 +262,8 @@ class _PostListNotifierProviderElement
@override
String? get pubName => (origin as PostListNotifierProvider).pubName;
@override
String? get realm => (origin as PostListNotifierProvider).realm;
@override
int? get type => (origin as PostListNotifierProvider).type;
@override
List<String>? get categories =>

View File

@@ -582,12 +582,19 @@ class PostHeader extends StatelessWidget {
else
...([
const Icon(Symbols.arrow_right, size: 14),
InkWell(
Flexible(
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
Text(item.realm!.name),
Flexible(
child: Text(
item.realm!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ProfilePictureWidget(
file: item.realm!.picture,
fallbackIcon: Symbols.group,
@@ -602,6 +609,7 @@ class PostHeader extends StatelessWidget {
);
},
),
),
]),
],
),