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:flutter/material.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/services/color.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:palette_generator/palette_generator.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -78,39 +80,131 @@ class RealmDetailScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0), 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( return AppScaffold(
isNoBackground: false, isNoBackground: false,
body: realmState.when( appBar:
loading: () => const Center(child: CircularProgressIndicator()), isWideScreen(context)
error: (error, _) => Center(child: Text('Error: $error')), ? realmState.when(
data: data:
(realm) => CustomScrollView( (realm) => AppBar(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
foregroundColor: appbarColor.value, foregroundColor: appbarColor.value,
leading: PageBackButton( leading: PageBackButton(
color: appbarColor.value, color: appbarColor.value,
shadows: [iconShadow], shadows: [iconShadow],
), ),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: Stack(
background: children: [
Positioned.fill(
child:
realm!.background?.id != null realm!.background?.id != null
? CloudImageWidget(fileId: realm.background!.id) ? CloudImageWidget(
fileId: realm.background!.id,
)
: Container( : Container(
color: color:
Theme.of(context).appBarTheme.backgroundColor, Theme.of(
context,
).appBarTheme.backgroundColor,
), ),
),
FlexibleSpaceBar(
title: Text( title: Text(
realm.name, realm.name,
style: TextStyle( style: TextStyle(
color: color:
appbarColor.value ?? appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor, Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
background: Container(),
),
],
), ),
actions: [ actions: [
IconButton( IconButton(
@@ -125,106 +219,144 @@ class RealmDetailScreen extends HookConsumerWidget {
); );
}, },
), ),
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow), _RealmActionMenu(
realmSlug: slug,
iconShadow: iconShadow,
),
const Gap(8), const Gap(8),
], ],
), ),
SliverToBoxAdapter( error: (_, _) => AppBar(leading: PageBackButton()),
child: ref loading: () => AppBar(leading: PageBackButton()),
.watch(realmIdentityProvider(slug)) )
.when( : 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(), loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
data: data:
(identity) => Column( (identity) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment:
children: [
ExpansionTile(
title: const Text('description').tr(),
initiallyExpanded: identity == null,
tilePadding: EdgeInsets.symmetric(
horizontal: 20,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.stretch, CrossAxisAlignment.stretch,
children: [ children: [
Text( realmDescriptionWidget(realm!),
realm.description, if (identity == null &&
style: const TextStyle(fontSize: 16), realm.isCommunity)
).padding( realmActionWidget(realm)
horizontal: 20, else
bottom: 16, const SizedBox.shrink(),
top: 8, ],
),
),
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) actions: [
FilledButton.tonalIcon( IconButton(
onPressed: () async { icon: Icon(Icons.people, shadows: [iconShadow]),
try { onPressed: () {
final apiClient = ref.read( showModalBottomSheet(
apiClientProvider, 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(), _RealmActionMenu(
).padding(horizontal: 16, vertical: 16) 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 else
const SizedBox.shrink(), const SizedBox.shrink(),
], ],
), ),
), ),
), ),
const SliverToBoxAdapter(child: Divider(height: 1)), SliverToBoxAdapter(
Consumer( child: realmChatRoomListWidget(realm),
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),
);
},
);
},
), ),
SliverPostList(realm: slug),
], ],
), ),
), ),
@@ -508,13 +640,8 @@ class _RealmMemberListSheet extends HookConsumerWidget {
} }
} }
return Container( Widget _buildMemberListHeader() {
constraints: BoxConstraints( return Padding(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row( child: Row(
children: [ children: [
@@ -547,9 +674,11 @@ class _RealmMemberListSheet extends HookConsumerWidget {
), ),
], ],
), ),
), );
const Divider(height: 1), }
Expanded(
Widget _buildMemberListContent() {
return Expanded(
child: PagingHelperView( child: PagingHelperView(
provider: memberListProvider, provider: memberListProvider,
futureRefreshable: memberListProvider.future, futureRefreshable: memberListProvider.future,
@@ -624,9 +753,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
).then((confirm) async { ).then((confirm) async {
if (confirm != true) return; if (confirm != true) return;
try { try {
final apiClient = ref.watch( final apiClient = ref.watch(apiClientProvider);
apiClientProvider,
);
await apiClient.delete( await apiClient.delete(
'/sphere/realms/$realmSlug/members/${member.accountId}', '/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; static const int _pageSize = 20;
@override @override
Future<CursorPagingData<SnPost>> build( Future<CursorPagingData<SnPost>> build({
String? pubName, { String? pubName,
String? realm,
int? type, int? type,
List<String>? categories, List<String>? categories,
List<String>? tags, List<String>? tags,
@@ -33,6 +34,7 @@ class PostListNotifier extends _$PostListNotifier
'offset': offset, 'offset': offset,
'take': _pageSize, 'take': _pageSize,
if (pubName != null) 'pub': pubName, if (pubName != null) 'pub': pubName,
if (realm != null) 'realm': realm,
if (type != null) 'type': type, if (type != null) 'type': type,
if (tags != null) 'tags': tags, if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories, if (categories != null) 'categories': categories,
@@ -68,6 +70,7 @@ enum PostItemType {
class SliverPostList extends HookConsumerWidget { class SliverPostList extends HookConsumerWidget {
final String? pubName; final String? pubName;
final String? realm;
final int? type; final int? type;
final List<String>? categories; final List<String>? categories;
final List<String>? tags; final List<String>? tags;
@@ -81,6 +84,7 @@ class SliverPostList extends HookConsumerWidget {
const SliverPostList({ const SliverPostList({
super.key, super.key,
this.pubName, this.pubName,
this.realm,
this.type, this.type,
this.categories, this.categories,
this.tags, this.tags,
@@ -96,21 +100,24 @@ class SliverPostList extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView( return PagingHelperSliverView(
provider: postListNotifierProvider( provider: postListNotifierProvider(
pubName, pubName: pubName,
realm: realm,
type: type, type: type,
categories: categories, categories: categories,
tags: tags, tags: tags,
), ),
futureRefreshable: futureRefreshable:
postListNotifierProvider( postListNotifierProvider(
pubName, pubName: pubName,
realm: realm,
type: type, type: type,
categories: categories, categories: categories,
tags: tags, tags: tags,
).future, ).future,
notifierRefreshable: notifierRefreshable:
postListNotifierProvider( postListNotifierProvider(
pubName, pubName: pubName,
realm: realm,
type: type, type: type,
categories: categories, categories: categories,
tags: tags, tags: tags,

View File

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

View File

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