♻️ Replaced all list with own pagination list

This commit is contained in:
2025-12-06 02:29:11 +08:00
parent c4ac256896
commit fd79c11d18
25 changed files with 1028 additions and 3073 deletions

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -6,134 +5,55 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
// Post Categories Notifier
final postCategoriesNotifierProvider = StateNotifierProvider.autoDispose<
final postCategoriesNotifierProvider = AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
AsyncValue<CursorPagingData<SnPostCategory>>
>((ref) {
return PostCategoriesNotifier(ref);
});
List<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostCategory>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
extends AutoDisposeAsyncNotifier<List<SnPostCategory>>
with AutoDisposeAsyncPaginationController<SnPostCategory> {
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
PostCategoriesNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final categories =
data.map((json) => SnPostCategory.fromJson(json)).toList();
final hasMore = categories.length == _pageSize;
final nextCursor =
hasMore ? (offset + categories.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...categories],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostCategory.fromJson(json)).toList();
}
}
// Post Tags Notifier
final postTagsNotifierProvider = StateNotifierProvider.autoDispose<
PostTagsNotifier,
AsyncValue<CursorPagingData<SnPostTag>>
>((ref) {
return PostTagsNotifier(ref);
});
class PostTagsNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostTag>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
PostTagsNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
final postTagsNotifierProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
class PostTagsNotifier extends AutoDisposeAsyncNotifier<List<SnPostTag>>
with AutoDisposeAsyncPaginationController<SnPostTag> {
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final tags = data.map((json) => SnPostTag.fromJson(json)).toList();
final hasMore = tags.length == _pageSize;
final nextCursor = hasMore ? (offset + tags.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...tags],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostTag.fromJson(json)).toList();
}
}
@@ -142,48 +62,27 @@ class PostCategoriesListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final categoriesState = ref.watch(postCategoriesNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('categories').tr()),
body: categoriesState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No categories found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postCategoriesNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final category = data.items[index];
return ListTile(
leading: const Icon(Symbols.category),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
title: Text(category.categoryDisplayTitle),
subtitle: Text('postCount'.plural(category.usage)),
onTap: () {
context.pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
body: PaginationList(
provider: postCategoriesNotifierProvider,
notifier: postCategoriesNotifierProvider.notifier,
padding: EdgeInsets.zero,
itemBuilder: (context, index, category) {
return ListTile(
leading: const Icon(Symbols.category),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
title: Text(category.categoryDisplayTitle),
subtitle: Text('postCount'.plural(category.usage)),
onTap: () {
context.pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postCategoriesNotifierProvider),
),
),
);
}
@@ -194,48 +93,27 @@ class PostTagsListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tagsState = ref.watch(postTagsNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('tags').tr()),
body: tagsState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No tags found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postTagsNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final tag = data.items[index];
return ListTile(
title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.label),
trailing: const Icon(Symbols.chevron_right),
subtitle: Text('postCount'.plural(tag.usage)),
onTap: () {
context.pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
body: PaginationList(
provider: postTagsNotifierProvider,
notifier: postTagsNotifierProvider.notifier,
padding: EdgeInsets.zero,
itemBuilder: (context, index, tag) {
return ListTile(
title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.label),
trailing: const Icon(Symbols.chevron_right),
subtitle: Text('postCount'.plural(tag.usage)),
onTap: () {
context.pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postTagsNotifierProvider),
),
),
);
}

View File

@@ -7,18 +7,18 @@ import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:styled_widget/styled_widget.dart';
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
PostSearchNotifier,
AsyncValue<CursorPagingData<SnPost>>
>((ref) => PostSearchNotifier(ref));
final postSearchNotifierProvider =
AsyncNotifierProvider.autoDispose<PostSearchNotifier, List<SnPost>>(
PostSearchNotifier.new,
);
class PostSearchNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPost>>> {
final AutoDisposeRef ref;
class PostSearchNotifier extends AutoDisposeAsyncNotifier<List<SnPost>>
with AutoDisposeAsyncPaginationController<SnPost> {
static const int _pageSize = 20;
String _currentQuery = '';
String? _pubName;
@@ -28,12 +28,13 @@ class PostSearchNotifier
List<String>? _tags;
bool _shuffle = false;
bool? _pinned;
bool _isLoading = false;
PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
@override
FutureOr<List<SnPost>> build() async {
// Initial state is empty if no query/filters, or fetch if needed
// But original logic allowed initial empty state.
// Let's replicate original logic: return empty list initially if no query.
return [];
}
Future<void> search(
@@ -46,8 +47,6 @@ class PostSearchNotifier
bool shuffle = false,
bool? pinned,
}) async {
if (_isLoading) return;
_currentQuery = query.trim();
_pubName = pubName;
_realm = realm;
@@ -57,7 +56,6 @@ class PostSearchNotifier
_shuffle = shuffle;
_pinned = pinned;
// Allow search even with empty query if any filters are applied
final hasFilters =
pubName != null ||
realm != null ||
@@ -68,59 +66,38 @@ class PostSearchNotifier
pinned != null;
if (_currentQuery.isEmpty && !hasFilters) {
state = AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
state = const AsyncData([]);
totalCount = null;
return;
}
await fetch(cursor: null);
await refresh();
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
@override
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
_isLoading = true;
state = const AsyncValue.loading();
final response = await client.get(
'/sphere/posts',
queryParameters: {
'query': _currentQuery,
'offset': fetchedCount,
'take': _pageSize,
'vector': false,
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,
if (_shuffle) 'shuffle': true,
if (_pinned != null) 'pinned': _pinned,
},
);
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts',
queryParameters: {
'query': _currentQuery,
'offset': offset,
'take': _pageSize,
'vector': false,
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,
if (_shuffle) 'shuffle': true,
if (_pinned != null) 'pinned': _pinned,
},
);
final data = response.data as List;
final posts = data.map((json) => SnPost.fromJson(json)).toList();
final hasMore = posts.length == _pageSize;
final nextCursor = hasMore ? (offset + posts.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: posts,
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPost.fromJson(json)).toList();
}
}
@@ -339,55 +316,34 @@ class PostSearchScreen extends HookConsumerWidget {
),
),
),
searchState.when(
data: (data) {
if (data.items.isEmpty && searchController.text.isNotEmpty) {
return SliverFillRemaining(
child: Center(child: Text('noResultsFound'.tr())),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index >= data.items.length) {
ref
.read(postSearchNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return Center(child: CircularProgressIndicator());
}
final post = data.items[index];
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: PostActionableItem(
item: post,
borderRadius: 8,
),
),
// Use PaginationList with isSliver=true
PaginationList(
provider: postSearchNotifierProvider,
notifier: postSearchNotifierProvider.notifier,
isSliver: true,
isRefreshable:
false, // CustomScrollView handles refreshing usually, but here we don't have PullToRefresh
itemBuilder: (context, index, post) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
);
}, childCount: data.items.length + (data.hasMore ? 1 : 0)),
);
},
loading:
() => SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
),
error:
(error, stack) => SliverFillRemaining(
child: ResponseErrorWidget(
error: error,
onRetry:
() => ref.invalidate(postSearchNotifierProvider),
child: PostActionableItem(item: post, borderRadius: 8),
),
),
);
},
),
if (searchState.valueOrNull?.isEmpty == true &&
searchController.text.isNotEmpty &&
!searchState.isLoading)
SliverFillRemaining(
child: Center(child: Text('noResultsFound'.tr())),
),
],
);
},

View File

@@ -22,47 +22,51 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:styled_widget/styled_widget.dart';
part 'realm_detail.g.dart';
final realmAppbarForegroundColorProvider = FutureProvider.autoDispose
.family<Color?, String>((ref, realmSlug) async {
final realm = await ref.watch(realmProvider(realmSlug).future);
if (realm?.background == null) return null;
final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider(
fileId: realm!.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
if (colors.isEmpty) return null;
final dominantColor = colors.first;
return dominantColor.computeLuminance() > 0.5
? Colors.black
: Colors.white;
});
@riverpod
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
final realm = await ref.watch(realmProvider(realmSlug).future);
if (realm?.background == null) return null;
final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider(
fileId: realm!.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
if (colors.isEmpty) return null;
final dominantColor = colors.first;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
final realmIdentityProvider = FutureProvider.autoDispose
.family<SnRealmMember?, String>((ref, realmSlug) async {
try {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get(
'/pass/realms/$realmSlug/members/me',
);
return SnRealmMember.fromJson(response.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null; // No identity found, user is not a member
}
rethrow;
}
});
@riverpod
Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async {
try {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/pass/realms/$realmSlug/members/me');
return SnRealmMember.fromJson(response.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null; // No identity found, user is not a member
}
rethrow;
}
}
@riverpod
Future<List<SnChatRoom>> realmChatRooms(Ref ref, String realmSlug) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/sphere/realms/$realmSlug/chat');
return (response.data as List).map((e) => SnChatRoom.fromJson(e)).toList();
}
final realmChatRoomsProvider = FutureProvider.autoDispose
.family<List<SnChatRoom>, String>((ref, realmSlug) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/sphere/realms/$realmSlug/chat');
return (response.data as List)
.map((e) => SnChatRoom.fromJson(e))
.toList();
});
class RealmDetailScreen extends HookConsumerWidget {
final String slug;
@@ -520,49 +524,32 @@ class _RealmActionMenu extends HookConsumerWidget {
}
}
@riverpod
class RealmMemberListNotifier extends _$RealmMemberListNotifier
with CursorPagingNotifierMixin<SnRealmMember> {
final realmMemberListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<RealmMemberListNotifier, List<SnRealmMember>, String>(
RealmMemberListNotifier.new,
);
class RealmMemberListNotifier
extends AutoDisposeFamilyAsyncNotifier<List<SnRealmMember>, String>
with FamilyAsyncPaginationController<SnRealmMember, String> {
static const int _pageSize = 20;
ValueNotifier<int> totalCount = ValueNotifier(0);
@override
Future<CursorPagingData<SnRealmMember>> build(String realmSlug) async {
totalCount.value = 0;
return fetch();
}
@override
Future<CursorPagingData<SnRealmMember>> fetch({String? cursor}) async {
Future<List<SnRealmMember>> fetch() async {
final apiClient = ref.read(apiClientProvider);
final offset = cursor != null ? int.parse(cursor) : 0;
final response = await apiClient.get(
'/pass/realms/$realmSlug/members',
'/pass/realms/$arg/members',
queryParameters: {
'offset': offset,
'offset': fetchedCount,
'take': _pageSize,
'withStatus': true,
},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
totalCount.value = total;
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnRealmMember.fromJson(e)).toList();
final hasMore = offset + members.length < total;
final nextCursor = hasMore ? (offset + members.length).toString() : null;
return CursorPagingData(
items: members,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
void dispose() {
totalCount.dispose();
return data.map((e) => SnRealmMember.fromJson(e)).toList();
}
}
@@ -574,11 +561,10 @@ class _RealmMemberListSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
final memberListProvider = realmMemberListNotifierProvider(realmSlug);
final memberListNotifier = ref.watch(memberListProvider.notifier);
useEffect(() {
return memberListNotifier.dispose;
}, []);
// memberListNotifier is not watched here to prevent unnecessary rebuilds of this widget
// when we only need it for passing to PaginationList as a Refreshable
// However, we used useEffect to dispose it, but AutoDispose handles it.
// So we remove the useEffect and the watch.
Future<void> invitePerson() async {
final result = await showModalBottomSheet(
@@ -606,17 +592,19 @@ class _RealmMemberListSheet extends HookConsumerWidget {
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
ListenableBuilder(
listenable: memberListNotifier.totalCount,
builder:
(context, _) => Text(
'members'.plural(memberListNotifier.totalCount.value),
key: ValueKey(memberListNotifier),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
Consumer(
builder: (context, ref, _) {
// effective watch to rebuild when data changes (and totalCount updates)
ref.watch(memberListProvider);
final notifier = ref.read(memberListProvider.notifier);
return Text(
'members'.plural(notifier.totalCount ?? 0),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
);
},
),
const Spacer(),
IconButton(
@@ -643,105 +631,94 @@ class _RealmMemberListSheet extends HookConsumerWidget {
Widget buildMemberListContent() {
return Expanded(
child: PagingHelperView(
child: PaginationList(
provider: memberListProvider,
futureRefreshable: memberListProvider.future,
notifierRefreshable: memberListProvider.notifier,
contentBuilder: (data, widgetCount, endItemView) {
return ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == data.items.length) {
return endItemView;
}
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: AccountPfcGestureDetector(
uname: member.account!.name,
child: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
notifier: memberListProvider.notifier,
itemBuilder: (context, index, member) {
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: AccountPfcGestureDetector(
uname: member.account!.name,
child: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
),
title: Row(
spacing: 6,
children: [
Flexible(
child: Text(
member.account!.nick,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
title: Row(
spacing: 6,
children: [
Flexible(
child: Text(
member.account!.nick,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (member.status != null)
AccountStatusLabel(status: member.status!),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Row(
children: [
Text(
member.role >= 100
? 'permissionOwner'
: member.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ((realmIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _RealmMemberRoleSheet(
realmSlug: realmSlug,
member: member,
),
).then((value) {
if (value != null) {
// Refresh the provider
ref.invalidate(memberListProvider);
}
});
},
),
if ((realmIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removeRealmMemberHint'.tr(),
'removeRealmMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/pass/realms/$realmSlug/members/${member.accountId}',
);
// Refresh the provider
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
if (member.status != null)
AccountStatusLabel(status: member.status!),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Row(
children: [
Text(
member.role >= 100
? 'permissionOwner'
: member.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ((realmIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _RealmMemberRoleSheet(
realmSlug: realmSlug,
member: member,
),
).then((value) {
if (value != null) {
// Refresh the provider
ref.invalidate(memberListProvider);
}
});
},
),
if ((realmIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removeRealmMemberHint'.tr(),
'removeRealmMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/pass/realms/$realmSlug/members/${member.accountId}',
);
// Refresh the provider
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
),

View File

@@ -1,552 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'realm_detail.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$realmAppbarForegroundColorHash() =>
r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [realmAppbarForegroundColor].
@ProviderFor(realmAppbarForegroundColor)
const realmAppbarForegroundColorProvider = RealmAppbarForegroundColorFamily();
/// See also [realmAppbarForegroundColor].
class RealmAppbarForegroundColorFamily extends Family<AsyncValue<Color?>> {
/// See also [realmAppbarForegroundColor].
const RealmAppbarForegroundColorFamily();
/// See also [realmAppbarForegroundColor].
RealmAppbarForegroundColorProvider call(String realmSlug) {
return RealmAppbarForegroundColorProvider(realmSlug);
}
@override
RealmAppbarForegroundColorProvider getProviderOverride(
covariant RealmAppbarForegroundColorProvider provider,
) {
return call(provider.realmSlug);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'realmAppbarForegroundColorProvider';
}
/// See also [realmAppbarForegroundColor].
class RealmAppbarForegroundColorProvider
extends AutoDisposeFutureProvider<Color?> {
/// See also [realmAppbarForegroundColor].
RealmAppbarForegroundColorProvider(String realmSlug)
: this._internal(
(ref) => realmAppbarForegroundColor(
ref as RealmAppbarForegroundColorRef,
realmSlug,
),
from: realmAppbarForegroundColorProvider,
name: r'realmAppbarForegroundColorProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmAppbarForegroundColorHash,
dependencies: RealmAppbarForegroundColorFamily._dependencies,
allTransitiveDependencies:
RealmAppbarForegroundColorFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmAppbarForegroundColorProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
Override overrideWith(
FutureOr<Color?> Function(RealmAppbarForegroundColorRef provider) create,
) {
return ProviderOverride(
origin: this,
override: RealmAppbarForegroundColorProvider._internal(
(ref) => create(ref as RealmAppbarForegroundColorRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeFutureProviderElement<Color?> createElement() {
return _RealmAppbarForegroundColorProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmAppbarForegroundColorProvider &&
other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmAppbarForegroundColorRef on AutoDisposeFutureProviderRef<Color?> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmAppbarForegroundColorProviderElement
extends AutoDisposeFutureProviderElement<Color?>
with RealmAppbarForegroundColorRef {
_RealmAppbarForegroundColorProviderElement(super.provider);
@override
String get realmSlug =>
(origin as RealmAppbarForegroundColorProvider).realmSlug;
}
String _$realmIdentityHash() => r'd5a3ecc6eeec291cebbfc9a45d8aac7195366381';
/// See also [realmIdentity].
@ProviderFor(realmIdentity)
const realmIdentityProvider = RealmIdentityFamily();
/// See also [realmIdentity].
class RealmIdentityFamily extends Family<AsyncValue<SnRealmMember?>> {
/// See also [realmIdentity].
const RealmIdentityFamily();
/// See also [realmIdentity].
RealmIdentityProvider call(String realmSlug) {
return RealmIdentityProvider(realmSlug);
}
@override
RealmIdentityProvider getProviderOverride(
covariant RealmIdentityProvider provider,
) {
return call(provider.realmSlug);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'realmIdentityProvider';
}
/// See also [realmIdentity].
class RealmIdentityProvider extends AutoDisposeFutureProvider<SnRealmMember?> {
/// See also [realmIdentity].
RealmIdentityProvider(String realmSlug)
: this._internal(
(ref) => realmIdentity(ref as RealmIdentityRef, realmSlug),
from: realmIdentityProvider,
name: r'realmIdentityProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmIdentityHash,
dependencies: RealmIdentityFamily._dependencies,
allTransitiveDependencies:
RealmIdentityFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmIdentityProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
Override overrideWith(
FutureOr<SnRealmMember?> Function(RealmIdentityRef provider) create,
) {
return ProviderOverride(
origin: this,
override: RealmIdentityProvider._internal(
(ref) => create(ref as RealmIdentityRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeFutureProviderElement<SnRealmMember?> createElement() {
return _RealmIdentityProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmIdentityProvider && other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmIdentityRef on AutoDisposeFutureProviderRef<SnRealmMember?> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmIdentityProviderElement
extends AutoDisposeFutureProviderElement<SnRealmMember?>
with RealmIdentityRef {
_RealmIdentityProviderElement(super.provider);
@override
String get realmSlug => (origin as RealmIdentityProvider).realmSlug;
}
String _$realmChatRoomsHash() => r'5f199906fb287b109e2a2d2a81dcb6675bdcb816';
/// See also [realmChatRooms].
@ProviderFor(realmChatRooms)
const realmChatRoomsProvider = RealmChatRoomsFamily();
/// See also [realmChatRooms].
class RealmChatRoomsFamily extends Family<AsyncValue<List<SnChatRoom>>> {
/// See also [realmChatRooms].
const RealmChatRoomsFamily();
/// See also [realmChatRooms].
RealmChatRoomsProvider call(String realmSlug) {
return RealmChatRoomsProvider(realmSlug);
}
@override
RealmChatRoomsProvider getProviderOverride(
covariant RealmChatRoomsProvider provider,
) {
return call(provider.realmSlug);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'realmChatRoomsProvider';
}
/// See also [realmChatRooms].
class RealmChatRoomsProvider
extends AutoDisposeFutureProvider<List<SnChatRoom>> {
/// See also [realmChatRooms].
RealmChatRoomsProvider(String realmSlug)
: this._internal(
(ref) => realmChatRooms(ref as RealmChatRoomsRef, realmSlug),
from: realmChatRoomsProvider,
name: r'realmChatRoomsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmChatRoomsHash,
dependencies: RealmChatRoomsFamily._dependencies,
allTransitiveDependencies:
RealmChatRoomsFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmChatRoomsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
Override overrideWith(
FutureOr<List<SnChatRoom>> Function(RealmChatRoomsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: RealmChatRoomsProvider._internal(
(ref) => create(ref as RealmChatRoomsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnChatRoom>> createElement() {
return _RealmChatRoomsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmChatRoomsProvider && other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmChatRoomsRef on AutoDisposeFutureProviderRef<List<SnChatRoom>> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmChatRoomsProviderElement
extends AutoDisposeFutureProviderElement<List<SnChatRoom>>
with RealmChatRoomsRef {
_RealmChatRoomsProviderElement(super.provider);
@override
String get realmSlug => (origin as RealmChatRoomsProvider).realmSlug;
}
String _$realmMemberListNotifierHash() =>
r'ab38c550c43cbf93d4c3e92e6658d76f40252c1f';
abstract class _$RealmMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> {
late final String realmSlug;
FutureOr<CursorPagingData<SnRealmMember>> build(String realmSlug);
}
/// See also [RealmMemberListNotifier].
@ProviderFor(RealmMemberListNotifier)
const realmMemberListNotifierProvider = RealmMemberListNotifierFamily();
/// See also [RealmMemberListNotifier].
class RealmMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnRealmMember>>> {
/// See also [RealmMemberListNotifier].
const RealmMemberListNotifierFamily();
/// See also [RealmMemberListNotifier].
RealmMemberListNotifierProvider call(String realmSlug) {
return RealmMemberListNotifierProvider(realmSlug);
}
@override
RealmMemberListNotifierProvider getProviderOverride(
covariant RealmMemberListNotifierProvider provider,
) {
return call(provider.realmSlug);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'realmMemberListNotifierProvider';
}
/// See also [RealmMemberListNotifier].
class RealmMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
> {
/// See also [RealmMemberListNotifier].
RealmMemberListNotifierProvider(String realmSlug)
: this._internal(
() => RealmMemberListNotifier()..realmSlug = realmSlug,
from: realmMemberListNotifierProvider,
name: r'realmMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmMemberListNotifierHash,
dependencies: RealmMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
RealmMemberListNotifierFamily._allTransitiveDependencies,
realmSlug: realmSlug,
);
RealmMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.realmSlug,
}) : super.internal();
final String realmSlug;
@override
FutureOr<CursorPagingData<SnRealmMember>> runNotifierBuild(
covariant RealmMemberListNotifier notifier,
) {
return notifier.build(realmSlug);
}
@override
Override overrideWith(RealmMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: RealmMemberListNotifierProvider._internal(
() => create()..realmSlug = realmSlug,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
realmSlug: realmSlug,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
>
createElement() {
return _RealmMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is RealmMemberListNotifierProvider &&
other.realmSlug == realmSlug;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, realmSlug.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin RealmMemberListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnRealmMember>> {
/// The parameter `realmSlug` of this provider.
String get realmSlug;
}
class _RealmMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
RealmMemberListNotifier,
CursorPagingData<SnRealmMember>
>
with RealmMemberListNotifierRef {
_RealmMemberListNotifierProviderElement(super.provider);
@override
String get realmSlug => (origin as RealmMemberListNotifierProvider).realmSlug;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package