♻️ Continued to move riverpod paging utils to own pagination utils

This commit is contained in:
2025-12-06 00:33:06 +08:00
parent 29574ada88
commit c1fc8ea3fe
12 changed files with 602 additions and 1304 deletions

View File

@@ -27,7 +27,7 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
int? totalCount;
@override
int fetchedCount = 0;
int get fetchedCount => state.value?.length ?? 0;
@override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
@@ -38,14 +38,12 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
@override
Future<void> refresh() async {
totalCount = null;
fetchedCount = 0;
state = AsyncData<List<T>>([]);
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
state = newState;
fetchedCount = newState.value?.length ?? 0;
}
@override
@@ -60,7 +58,47 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
});
state = newState;
fetchedCount = newState.value?.length ?? 0;
}
}
mixin FamilyAsyncPaginationController<T, Arg>
on AutoDisposeFamilyAsyncNotifier<List<T>, Arg>
implements PaginationController<T> {
@override
int? totalCount;
@override
int get fetchedCount => state.value?.length ?? 0;
@override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
@override
FutureOr<List<T>> build(Arg arg) async => fetch();
@override
Future<void> refresh() async {
totalCount = null;
state = AsyncData<List<T>>([]);
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
state = newState;
}
@override
Future<void> fetchFurther() async {
if (fetchedAll) return;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
final elements = await fetch();
return [...?state.valueOrNull, ...elements];
});
state = newState;
}
}
@@ -71,10 +109,8 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
if (currentFilter == filter) return;
// Reset the data
totalCount = null;
fetchedCount = 0;
currentFilter = filter;
state = AsyncData<List<T>>([]);
currentFilter = filter;
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();

View File

@@ -3,16 +3,17 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/paging.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/models/relationship.dart';
import 'package:island/pods/network.dart';
@@ -28,39 +29,31 @@ Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
.toList();
}
@riverpod
class RelationshipListNotifier extends _$RelationshipListNotifier
with CursorPagingNotifierMixin<SnRelationship> {
@override
Future<CursorPagingData<SnRelationship>> build() => fetch(cursor: null);
final relationshipListNotifierProvider = AsyncNotifierProvider(
RelationshipListNotifier.new,
);
class RelationshipListNotifier extends AsyncNotifier<List<SnRelationship>>
with AsyncPaginationController<SnRelationship> {
@override
Future<CursorPagingData<SnRelationship>> fetch({
required String? cursor,
}) async {
Future<List<SnRelationship>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
final response = await client.get(
'/pass/relationships',
queryParameters: {'offset': offset, 'take': take},
queryParameters: {'offset': fetchedCount.toString(), 'take': take},
);
final List<SnRelationship> items =
(response.data as List)
.map((e) => SnRelationship.fromJson(e as Map<String, dynamic>))
.cast<SnRelationship>()
.toList();
final total = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null;
totalCount = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
return items;
}
}
@@ -242,7 +235,7 @@ class RelationshipScreen extends HookConsumerWidget {
await client.post(
'/pass/relationships/${relationship.accountId}/friends/${isAccept ? 'accept' : 'decline'}',
);
relationshipNotifier.forceRefresh();
relationshipNotifier.refresh();
if (!context.mounted) return;
if (isAccept) {
showSnackBar(
@@ -270,7 +263,7 @@ class RelationshipScreen extends HookConsumerWidget {
'/pass/relationships/${relationship.accountId}',
data: {'status': newStatus},
);
relationshipNotifier.forceRefresh();
relationshipNotifier.refresh();
}
final user = ref.watch(userInfoProvider);
@@ -305,32 +298,20 @@ class RelationshipScreen extends HookConsumerWidget {
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
child: PaginationList(
provider: relationshipListNotifierProvider,
futureRefreshable: relationshipListNotifierProvider.future,
notifierRefreshable: relationshipListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final relationship = data.items[index];
return RelationshipListTile(
relationship: relationship,
submitting: submitting.value,
onAccept: () => handleFriendRequest(relationship, true),
onDecline:
() => handleFriendRequest(relationship, false),
currentUserId: user.value?.id,
showRelatedAccount: false,
onUpdateStatus: updateRelationship,
);
},
),
notifier: relationshipListNotifierProvider.notifier,
itemBuilder: (context, index, relationship) {
return RelationshipListTile(
relationship: relationship,
submitting: submitting.value,
onAccept: () => handleFriendRequest(relationship, true),
onDecline: () => handleFriendRequest(relationship, false),
currentUserId: user.value?.id,
showRelatedAccount: false,
onUpdateStatus: updateRelationship,
);
},
),
),
],

View File

@@ -26,26 +26,5 @@ final sentFriendRequestProvider =
// ignore: unused_element
typedef SentFriendRequestRef =
AutoDisposeFutureProviderRef<List<SnRelationship>>;
String _$relationshipListNotifierHash() =>
r'fc46920256f7c48445c00652165e879890f2c9a3';
/// See also [RelationshipListNotifier].
@ProviderFor(RelationshipListNotifier)
final relationshipListNotifierProvider = AutoDisposeAsyncNotifierProvider<
RelationshipListNotifier,
CursorPagingData<SnRelationship>
>.internal(
RelationshipListNotifier.new,
name: r'relationshipListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$relationshipListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$RelationshipListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnRelationship>>;
// 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

View File

@@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/chat/chat_room.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/account/status.dart';
@@ -17,9 +18,9 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/screens/chat/chat_form.dart';
import 'package:island/widgets/paging/pagination_list.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:styled_widget/styled_widget.dart';
import 'package:island/pods/database.dart';
import 'package:island/screens/chat/search_messages.dart';
@@ -600,39 +601,36 @@ class ChatMemberNotifier extends StateNotifier<ChatRoomMemberState> {
}
}
@riverpod
class ChatMemberListNotifier extends _$ChatMemberListNotifier
with CursorPagingNotifierMixin<SnChatMember> {
@override
Future<CursorPagingData<SnChatMember>> build(String roomId) {
return fetch();
}
final chatMemberListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<ChatMemberListNotifier, List<SnChatMember>, String>(
ChatMemberListNotifier.new,
);
class ChatMemberListNotifier
extends AutoDisposeFamilyAsyncNotifier<List<SnChatMember>, String>
with FamilyAsyncPaginationController<SnChatMember, String> {
static const pageSize = 20;
@override
Future<CursorPagingData<SnChatMember>> fetch({String? cursor}) async {
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
Future<List<SnChatMember>> fetch() async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get(
'/sphere/chat/$roomId/members',
queryParameters: {'offset': offset, 'take': take, 'withStatus': true},
'/sphere/chat/$arg/members',
queryParameters: {
'offset': fetchedCount.toString(),
'take': pageSize,
'withStatus': true,
},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnChatMember.fromJson(e)).toList();
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final members =
response.data
.map((e) => SnChatMember.fromJson(e))
.cast<SnChatMember>()
.toList();
// Calculate next cursor based on total count
final nextOffset = offset + members.length;
final String? nextCursor =
nextOffset < total ? nextOffset.toString() : null;
return CursorPagingData(
items: members,
nextCursor: nextCursor,
hasMore: members.length < total,
);
return members;
}
}
@@ -727,75 +725,62 @@ class _ChatMemberListSheet extends HookConsumerWidget {
),
const Divider(height: 1),
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, idx, 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)),
if (member.status != null)
AccountStatusLabel(
status: member.status!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account.nick)),
if (member.status != null)
AccountStatusLabel(
status: member.status!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Text("@${member.account.name}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isManagable)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removeChatMemberHint'.tr(),
'removeChatMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/sphere/chat/$roomId/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Text("@${member.account.name}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isManagable)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removeChatMemberHint'.tr(),
'removeChatMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/sphere/chat/$roomId/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
),

View File

@@ -149,154 +149,5 @@ class _TotalMessagesCountProviderElement
String get roomId => (origin as TotalMessagesCountProvider).roomId;
}
String _$chatMemberListNotifierHash() =>
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
abstract class _$ChatMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
late final String roomId;
FutureOr<CursorPagingData<SnChatMember>> build(String roomId);
}
/// See also [ChatMemberListNotifier].
@ProviderFor(ChatMemberListNotifier)
const chatMemberListNotifierProvider = ChatMemberListNotifierFamily();
/// See also [ChatMemberListNotifier].
class ChatMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnChatMember>>> {
/// See also [ChatMemberListNotifier].
const ChatMemberListNotifierFamily();
/// See also [ChatMemberListNotifier].
ChatMemberListNotifierProvider call(String roomId) {
return ChatMemberListNotifierProvider(roomId);
}
@override
ChatMemberListNotifierProvider getProviderOverride(
covariant ChatMemberListNotifierProvider provider,
) {
return call(provider.roomId);
}
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'chatMemberListNotifierProvider';
}
/// See also [ChatMemberListNotifier].
class ChatMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
> {
/// See also [ChatMemberListNotifier].
ChatMemberListNotifierProvider(String roomId)
: this._internal(
() => ChatMemberListNotifier()..roomId = roomId,
from: chatMemberListNotifierProvider,
name: r'chatMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$chatMemberListNotifierHash,
dependencies: ChatMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
ChatMemberListNotifierFamily._allTransitiveDependencies,
roomId: roomId,
);
ChatMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
FutureOr<CursorPagingData<SnChatMember>> runNotifierBuild(
covariant ChatMemberListNotifier notifier,
) {
return notifier.build(roomId);
}
@override
Override overrideWith(ChatMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ChatMemberListNotifierProvider._internal(
() => create()..roomId = roomId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
>
createElement() {
return _ChatMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ChatMemberListNotifierProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ChatMemberListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnChatMember>> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _ChatMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ChatMemberListNotifier,
CursorPagingData<SnChatMember>
>
with ChatMemberListNotifierRef {
_ChatMemberListNotifierProviderElement(super.provider);
@override
String get roomId => (origin as ChatMemberListNotifierProvider).roomId;
}
// 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

View File

@@ -10,6 +10,7 @@ import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/heatmap.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/services/responsive.dart';
import 'package:island/utils/text.dart';
@@ -18,11 +19,11 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/response.dart';
import 'package:island/widgets/activity_heatmap.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:styled_widget/styled_widget.dart';
part 'hub.g.dart';
@@ -77,38 +78,31 @@ Future<List<SnPublisherMember>> publisherInvites(Ref ref) async {
.toList();
}
@riverpod
class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
with CursorPagingNotifierMixin<SnPublisherMember> {
static const int _pageSize = 20;
final publisherMemberListNotifierProvider = AsyncNotifierProvider.family
.autoDispose(PublisherMemberListNotifier.new);
class PublisherMemberListNotifier
extends AutoDisposeFamilyAsyncNotifier<List<SnPublisherMember>, String>
with FamilyAsyncPaginationController<SnPublisherMember, String> {
static const int pageSize = 20;
@override
Future<CursorPagingData<SnPublisherMember>> build(String uname) async {
return fetch();
}
@override
Future<CursorPagingData<SnPublisherMember>> fetch({String? cursor}) async {
Future<List<SnPublisherMember>> fetch() async {
final apiClient = ref.read(apiClientProvider);
final offset = cursor != null ? int.parse(cursor) : 0;
final response = await apiClient.get(
'/sphere/publishers/$uname/members',
queryParameters: {'offset': offset, 'take': _pageSize},
'/sphere/publishers/$arg/members',
queryParameters: {'offset': fetchedCount.toString(), 'take': pageSize},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final members =
response.data
.map((e) => SnPublisherMember.fromJson(e))
.cast<SnPublisherMember>()
.toList();
final hasMore = offset + members.length < total;
final nextCursor = hasMore ? (offset + members.length).toString() : null;
return CursorPagingData(
items: members,
hasMore: hasMore,
nextCursor: nextCursor,
);
return members;
}
}
@@ -902,100 +896,87 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
),
const Divider(height: 1),
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: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
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 ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname,
member: member,
),
).then((value) {
if (value != null) {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
}
});
},
),
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removePublisherMemberHint'.tr(),
'removePublisherMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/sphere/publishers/$publisherUname/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
notifier: memberListProvider.notifier,
itemBuilder: (context, index, member) {
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
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 ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname,
member: member,
),
).then((value) {
if (value != null) {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
}
});
},
),
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removePublisherMemberHint'.tr(),
'removePublisherMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/sphere/publishers/$publisherUname/members/${member.accountId}',
);
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
),

View File

@@ -534,158 +534,5 @@ final publisherInvitesProvider =
// ignore: unused_element
typedef PublisherInvitesRef =
AutoDisposeFutureProviderRef<List<SnPublisherMember>>;
String _$publisherMemberListNotifierHash() =>
r'b4afd5d591a6f3d29f1b45fb1b6d17cb34f3f11b';
abstract class _$PublisherMemberListNotifier
extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPublisherMember>> {
late final String uname;
FutureOr<CursorPagingData<SnPublisherMember>> build(String uname);
}
/// See also [PublisherMemberListNotifier].
@ProviderFor(PublisherMemberListNotifier)
const publisherMemberListNotifierProvider = PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPublisherMember>>> {
/// See also [PublisherMemberListNotifier].
const PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider call(String uname) {
return PublisherMemberListNotifierProvider(uname);
}
@override
PublisherMemberListNotifierProvider getProviderOverride(
covariant PublisherMemberListNotifierProvider provider,
) {
return call(provider.uname);
}
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'publisherMemberListNotifierProvider';
}
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
> {
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider(String uname)
: this._internal(
() => PublisherMemberListNotifier()..uname = uname,
from: publisherMemberListNotifierProvider,
name: r'publisherMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherMemberListNotifierHash,
dependencies: PublisherMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
PublisherMemberListNotifierFamily._allTransitiveDependencies,
uname: uname,
);
PublisherMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
FutureOr<CursorPagingData<SnPublisherMember>> runNotifierBuild(
covariant PublisherMemberListNotifier notifier,
) {
return notifier.build(uname);
}
@override
Override overrideWith(PublisherMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PublisherMemberListNotifierProvider._internal(
() => create()..uname = uname,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
createElement() {
return _PublisherMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherMemberListNotifierProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherMemberListNotifierRef
on
AutoDisposeAsyncNotifierProviderRef<
CursorPagingData<SnPublisherMember>
> {
/// The parameter `uname` of this provider.
String get uname;
}
class _PublisherMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
with PublisherMemberListNotifierRef {
_PublisherMemberListNotifierProviderElement(super.provider);
@override
String get uname => (origin as PublisherMemberListNotifierProvider).uname;
}
// 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

View File

@@ -7,16 +7,17 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -81,45 +82,37 @@ class NotificationUnreadCountNotifier
}
}
@riverpod
class NotificationListNotifier extends _$NotificationListNotifier
with CursorPagingNotifierMixin<SnNotification> {
static const int _pageSize = 5;
final notificationListNotifierProvider = AsyncNotifierProvider(
NotificationListNotifier.new,
);
class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
with AsyncPaginationController<SnNotification> {
static const int pageSize = 5;
@override
Future<CursorPagingData<SnNotification>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnNotification>> fetch({
required String? cursor,
}) async {
Future<List<SnNotification>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final queryParams = {'offset': fetchedCount.toString(), 'take': pageSize};
final response = await client.get(
'/ring/notifications',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final notifications =
data.map((json) => SnNotification.fromJson(json)).toList();
response.data
.map((json) => SnNotification.fromJson(json))
.cast<SnNotification>()
.toList();
final hasMore = offset + notifications.length < total;
final nextCursor =
hasMore ? (offset + notifications.length).toString() : null;
final unreadCount = notifications.where((n) => n.viewedAt == null).length;
ref
.read(notificationUnreadCountNotifierProvider.notifier)
.decrement(unreadCount);
return CursorPagingData(
items: notifications,
hasMore: hasMore,
nextCursor: nextCursor,
);
return notifications;
}
}
@@ -174,129 +167,108 @@ class NotificationSheet extends HookConsumerWidget {
icon: const Icon(Symbols.mark_as_unread),
),
],
child: PagingHelperView(
child: PaginationList(
provider: notificationListNotifierProvider,
futureRefreshable: notificationListNotifierProvider.future,
notifierRefreshable: notificationListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
notifier: notificationListNotifierProvider.notifier,
itemBuilder: (context, index, notification) {
final pfp = notification.meta['pfp'] as String?;
final images = notification.meta['images'] as List?;
final imageIds = images?.cast<String>() ?? [];
final notification = data.items[index];
final pfp = notification.meta['pfp'] as String?;
final images = notification.meta['images'] as List?;
final imageIds = images?.cast<String>() ?? [];
return ListTile(
isThreeLine: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading:
pfp != null
? ProfilePictureWidget(fileId: pfp, radius: 20)
: CircleAvatar(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
child: Icon(
_getNotificationIcon(notification.topic),
color:
Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
),
title: Text(notification.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (notification.subtitle.isNotEmpty)
Text(notification.subtitle).bold(),
Row(
spacing: 6,
children: [
Text(
DateFormat().format(
notification.createdAt.toLocal(),
),
).fontSize(11),
Text('·').fontSize(11).bold(),
Text(
RelativeTime(
context,
).format(notification.createdAt.toLocal()),
).fontSize(11),
],
).opacity(0.75).padding(bottom: 4),
MarkdownTextContent(
content: notification.content,
textStyle: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
),
return ListTile(
isThreeLine: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading:
pfp != null
? ProfilePictureWidget(fileId: pfp, radius: 20)
: CircleAvatar(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
child: Icon(
_getNotificationIcon(notification.topic),
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
if (imageIds.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children:
imageIds.map((imageId) {
return SizedBox(
width: 80,
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudImageWidget(
fileId: imageId,
aspectRatio: 1,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),
],
),
title: Text(notification.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (notification.subtitle.isNotEmpty)
Text(notification.subtitle).bold(),
Row(
spacing: 6,
children: [
Text(
DateFormat().format(notification.createdAt.toLocal()),
).fontSize(11),
Text('·').fontSize(11).bold(),
Text(
RelativeTime(
context,
).format(notification.createdAt.toLocal()),
).fontSize(11),
],
).opacity(0.75).padding(bottom: 4),
MarkdownTextContent(
content: notification.content,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
),
trailing:
notification.viewedAt != null
? null
: Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
);
},
),
if (imageIds.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children:
imageIds.map((imageId) {
return SizedBox(
width: 80,
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudImageWidget(
fileId: imageId,
aspectRatio: 1,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),
],
),
trailing:
notification.viewedAt != null
? null
: Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
);
},
),
);
}

View File

@@ -27,26 +27,5 @@ final notificationUnreadCountNotifierProvider =
);
typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
String _$notificationListNotifierHash() =>
r'260046e11f45b0d67ab25bcbdc8604890d71ccc7';
/// See also [NotificationListNotifier].
@ProviderFor(NotificationListNotifier)
final notificationListNotifierProvider = AutoDisposeAsyncNotifierProvider<
NotificationListNotifier,
CursorPagingData<SnNotification>
>.internal(
NotificationListNotifier.new,
name: r'notificationListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$notificationListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$NotificationListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnNotification>>;
// 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

View File

@@ -19,7 +19,8 @@ import 'package:island/widgets/payment/payment_overlay.dart';
import 'package:island/widgets/response.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';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
@@ -990,70 +991,87 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
}
}
@riverpod
class TransactionListNotifier extends _$TransactionListNotifier
with CursorPagingNotifierMixin<SnTransaction> {
static const int _pageSize = 20;
final transactionListNotifierProvider = AsyncNotifierProvider(
TransactionListNotifier.new,
);
class TransactionListNotifier extends AsyncNotifier<List<SnTransaction>>
with AsyncPaginationController<SnTransaction> {
static const int pageSize = 20;
@override
Future<CursorPagingData<SnTransaction>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnTransaction>> fetch({
required String? cursor,
}) async {
Future<List<SnTransaction>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final offset = fetchedCount;
final queryParams = {'offset': offset, 'take': _pageSize};
final queryParams = {'offset': offset, 'take': pageSize};
final response = await client.get(
'/pass/wallets/transactions',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final transactions =
data.map((json) => SnTransaction.fromJson(json)).toList();
final hasMore = offset + transactions.length < total;
final nextCursor =
hasMore ? (offset + transactions.length).toString() : null;
return CursorPagingData(
items: transactions,
hasMore: hasMore,
nextCursor: nextCursor,
);
return transactions;
}
}
@riverpod
Future<List<SnWalletFund>> walletFunds(
Ref ref, {
int offset = 0,
int take = 20,
}) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/pass/wallets/funds?offset=$offset&take=$take',
);
return (resp.data as List).map((e) => SnWalletFund.fromJson(e)).toList();
final walletFundsNotifierProvider = AsyncNotifierProvider(
WalletFundsNotifier.new,
);
class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>>
with AsyncPaginationController<SnWalletFund> {
static const int pageSize = 20;
@override
Future<List<SnWalletFund>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = fetchedCount;
final response = await client.get(
'/pass/wallets/funds?offset=$offset&take=$pageSize',
);
// Assuming total count header is present or we just check if list is empty
final list =
(response.data as List).map((e) => SnWalletFund.fromJson(e)).toList();
if (list.length < pageSize) {
totalCount = fetchedCount + list.length;
}
return list;
}
}
@riverpod
Future<List<SnWalletFundRecipient>> walletFundRecipients(
Ref ref, {
int offset = 0,
int take = 20,
}) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/pass/wallets/funds/recipients?offset=$offset&take=$take',
);
return (resp.data as List)
.map((e) => SnWalletFundRecipient.fromJson(e))
.toList();
final walletFundRecipientsNotifierProvider = AsyncNotifierProvider(
WalletFundRecipientsNotifier.new,
);
class WalletFundRecipientsNotifier
extends AsyncNotifier<List<SnWalletFundRecipient>>
with AsyncPaginationController<SnWalletFundRecipient> {
static const int _pageSize = 20;
@override
Future<List<SnWalletFundRecipient>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = fetchedCount;
final response = await client.get(
'/pass/wallets/funds/recipients?offset=$offset&take=$_pageSize',
);
final list =
(response.data as List)
.map((e) => SnWalletFundRecipient.fromJson(e))
.toList();
if (list.length < _pageSize) {
totalCount = fetchedCount + list.length;
}
return list;
}
}
@riverpod
@@ -1408,71 +1426,50 @@ class WalletScreen extends HookConsumerWidget {
controller: tabController,
children: [
// Transactions Tab
CustomScrollView(
slivers: [
PagingHelperSliverView(
provider: transactionListNotifierProvider,
futureRefreshable: transactionListNotifierProvider.future,
notifierRefreshable:
transactionListNotifierProvider.notifier,
contentBuilder:
(
data,
widgetCount,
endItemView,
) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
PaginationList(
padding: EdgeInsets.zero,
provider: transactionListNotifierProvider,
notifier: transactionListNotifierProvider.notifier,
itemBuilder: (context, index, transaction) {
final isIncome =
transaction.payeeWalletId == wallet.value?.id;
final transaction = data.items[index];
final isIncome =
transaction.payeeWalletId == wallet.value?.id;
return InkWell(
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) => TransactionDetailSheet(
transaction: transaction,
),
);
},
child: ListTile(
key: ValueKey(transaction.id),
leading: Icon(
isIncome
? Symbols.payment_arrow_down
: Symbols.paid,
),
title: Text(
transaction.remarks ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
DateFormat.yMd().add_Hm().format(
transaction.createdAt,
),
),
trailing: Text(
'${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}',
style: TextStyle(
color:
isIncome ? Colors.green : Colors.red,
),
),
),
);
},
return InkWell(
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) => TransactionDetailSheet(
transaction: transaction,
),
);
},
child: ListTile(
key: ValueKey(transaction.id),
leading: Icon(
isIncome ? Symbols.payment_arrow_down : Symbols.paid,
),
title: Text(
transaction.remarks ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
DateFormat.yMd().add_Hm().format(
transaction.createdAt,
),
),
],
),
trailing: Text(
'${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}',
style: TextStyle(
color: isIncome ? Colors.green : Colors.red,
),
),
),
);
},
),
// My Funds Tab
@@ -1522,7 +1519,7 @@ class WalletScreen extends HookConsumerWidget {
}
Widget _buildFundsList(BuildContext context, WidgetRef ref) {
final funds = ref.watch(walletFundsProvider());
final funds = ref.watch(walletFundsNotifierProvider);
return funds.when(
data: (fundList) {
@@ -1784,7 +1781,7 @@ class WalletScreen extends HookConsumerWidget {
if (paidOrder != null) {
// Wait for server to handle order
await Future.delayed(const Duration(seconds: 1));
ref.invalidate(walletFundsProvider);
ref.invalidate(walletFundsNotifierProvider);
ref.invalidate(walletCurrentProvider);
if (context.mounted) {
showSnackBar('fundCreatedSuccessfully'.tr());

View File

@@ -40,7 +40,7 @@ final walletStatsProvider = AutoDisposeFutureProvider<SnWalletStats>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef WalletStatsRef = AutoDisposeFutureProviderRef<SnWalletStats>;
String _$walletFundsHash() => r'f60718c01ca5b7618a02682a0417669f750644a3';
String _$walletFundHash() => r'459efdee5e2775eedaa4312e0d317c218fa7e1fa';
/// Copied from Dart SDK
class _SystemHash {
@@ -63,284 +63,6 @@ class _SystemHash {
}
}
/// See also [walletFunds].
@ProviderFor(walletFunds)
const walletFundsProvider = WalletFundsFamily();
/// See also [walletFunds].
class WalletFundsFamily extends Family<AsyncValue<List<SnWalletFund>>> {
/// See also [walletFunds].
const WalletFundsFamily();
/// See also [walletFunds].
WalletFundsProvider call({int offset = 0, int take = 20}) {
return WalletFundsProvider(offset: offset, take: take);
}
@override
WalletFundsProvider getProviderOverride(
covariant WalletFundsProvider provider,
) {
return call(offset: provider.offset, take: provider.take);
}
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'walletFundsProvider';
}
/// See also [walletFunds].
class WalletFundsProvider
extends AutoDisposeFutureProvider<List<SnWalletFund>> {
/// See also [walletFunds].
WalletFundsProvider({int offset = 0, int take = 20})
: this._internal(
(ref) => walletFunds(ref as WalletFundsRef, offset: offset, take: take),
from: walletFundsProvider,
name: r'walletFundsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$walletFundsHash,
dependencies: WalletFundsFamily._dependencies,
allTransitiveDependencies: WalletFundsFamily._allTransitiveDependencies,
offset: offset,
take: take,
);
WalletFundsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.offset,
required this.take,
}) : super.internal();
final int offset;
final int take;
@override
Override overrideWith(
FutureOr<List<SnWalletFund>> Function(WalletFundsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: WalletFundsProvider._internal(
(ref) => create(ref as WalletFundsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
offset: offset,
take: take,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnWalletFund>> createElement() {
return _WalletFundsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is WalletFundsProvider &&
other.offset == offset &&
other.take == take;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, offset.hashCode);
hash = _SystemHash.combine(hash, take.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin WalletFundsRef on AutoDisposeFutureProviderRef<List<SnWalletFund>> {
/// The parameter `offset` of this provider.
int get offset;
/// The parameter `take` of this provider.
int get take;
}
class _WalletFundsProviderElement
extends AutoDisposeFutureProviderElement<List<SnWalletFund>>
with WalletFundsRef {
_WalletFundsProviderElement(super.provider);
@override
int get offset => (origin as WalletFundsProvider).offset;
@override
int get take => (origin as WalletFundsProvider).take;
}
String _$walletFundRecipientsHash() =>
r'3a5e32b2d20700edd5944885693aff127b58adb1';
/// See also [walletFundRecipients].
@ProviderFor(walletFundRecipients)
const walletFundRecipientsProvider = WalletFundRecipientsFamily();
/// See also [walletFundRecipients].
class WalletFundRecipientsFamily
extends Family<AsyncValue<List<SnWalletFundRecipient>>> {
/// See also [walletFundRecipients].
const WalletFundRecipientsFamily();
/// See also [walletFundRecipients].
WalletFundRecipientsProvider call({int offset = 0, int take = 20}) {
return WalletFundRecipientsProvider(offset: offset, take: take);
}
@override
WalletFundRecipientsProvider getProviderOverride(
covariant WalletFundRecipientsProvider provider,
) {
return call(offset: provider.offset, take: provider.take);
}
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'walletFundRecipientsProvider';
}
/// See also [walletFundRecipients].
class WalletFundRecipientsProvider
extends AutoDisposeFutureProvider<List<SnWalletFundRecipient>> {
/// See also [walletFundRecipients].
WalletFundRecipientsProvider({int offset = 0, int take = 20})
: this._internal(
(ref) => walletFundRecipients(
ref as WalletFundRecipientsRef,
offset: offset,
take: take,
),
from: walletFundRecipientsProvider,
name: r'walletFundRecipientsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$walletFundRecipientsHash,
dependencies: WalletFundRecipientsFamily._dependencies,
allTransitiveDependencies:
WalletFundRecipientsFamily._allTransitiveDependencies,
offset: offset,
take: take,
);
WalletFundRecipientsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.offset,
required this.take,
}) : super.internal();
final int offset;
final int take;
@override
Override overrideWith(
FutureOr<List<SnWalletFundRecipient>> Function(
WalletFundRecipientsRef provider,
)
create,
) {
return ProviderOverride(
origin: this,
override: WalletFundRecipientsProvider._internal(
(ref) => create(ref as WalletFundRecipientsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
offset: offset,
take: take,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnWalletFundRecipient>>
createElement() {
return _WalletFundRecipientsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is WalletFundRecipientsProvider &&
other.offset == offset &&
other.take == take;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, offset.hashCode);
hash = _SystemHash.combine(hash, take.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin WalletFundRecipientsRef
on AutoDisposeFutureProviderRef<List<SnWalletFundRecipient>> {
/// The parameter `offset` of this provider.
int get offset;
/// The parameter `take` of this provider.
int get take;
}
class _WalletFundRecipientsProviderElement
extends AutoDisposeFutureProviderElement<List<SnWalletFundRecipient>>
with WalletFundRecipientsRef {
_WalletFundRecipientsProviderElement(super.provider);
@override
int get offset => (origin as WalletFundRecipientsProvider).offset;
@override
int get take => (origin as WalletFundRecipientsProvider).take;
}
String _$walletFundHash() => r'459efdee5e2775eedaa4312e0d317c218fa7e1fa';
/// See also [walletFund].
@ProviderFor(walletFund)
const walletFundProvider = WalletFundFamily();
@@ -459,26 +181,5 @@ class _WalletFundProviderElement
String get fundId => (origin as WalletFundProvider).fundId;
}
String _$transactionListNotifierHash() =>
r'74d3c15f45a6e55b36150ab38e98475a508fc932';
/// See also [TransactionListNotifier].
@ProviderFor(TransactionListNotifier)
final transactionListNotifierProvider = AutoDisposeAsyncNotifierProvider<
TransactionListNotifier,
CursorPagingData<SnTransaction>
>.internal(
TransactionListNotifier.new,
name: r'transactionListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$transactionListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TransactionListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnTransaction>>;
// 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

View File

@@ -22,6 +22,8 @@ class ComposeFundSheet extends HookConsumerWidget {
final isPushing = useState(false);
final errorText = useState<String?>(null);
final fundsData = ref.watch(walletFundsNotifierProvider);
return SheetScaffold(
heightFactor: 0.6,
titleText: 'fund'.tr(),
@@ -41,161 +43,146 @@ class ComposeFundSheet extends HookConsumerWidget {
child: TabBarView(
children: [
// Link/Select existing fund list
ref
.watch(walletFundsProvider())
.when(
data:
(funds) =>
funds.isEmpty
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Symbols.money_bag,
size: 48,
color:
Theme.of(
context,
).colorScheme.outline,
),
const Gap(16),
Text(
'noFundsCreated'.tr(),
style:
Theme.of(
context,
).textTheme.titleMedium,
),
],
fundsData.when(
data:
(funds) =>
funds.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Symbols.money_bag,
size: 48,
color:
Theme.of(
context,
).colorScheme.outline,
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: funds.length,
itemBuilder: (context, index) {
final fund = funds[index];
const Gap(16),
Text(
'noFundsCreated'.tr(),
style:
Theme.of(
context,
).textTheme.titleMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: funds.length,
itemBuilder: (context, index) {
final fund = funds[index];
return Card(
margin: const EdgeInsets.only(
bottom: 8,
),
child: InkWell(
onTap:
() => Navigator.of(
context,
).pop(fund),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap:
() =>
Navigator.of(context).pop(fund),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Row(
children: [
Icon(
Symbols.money_bag,
Icon(
Symbols.money_bag,
color:
Theme.of(
context,
).colorScheme.primary,
fill: 1,
),
const Gap(8),
Expanded(
child: Text(
'${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}',
style: TextStyle(
fontSize: 18,
fontWeight:
FontWeight.bold,
color:
Theme.of(context)
.colorScheme
.primary,
fill: 1,
),
const Gap(8),
Expanded(
child: Text(
'${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}',
style: TextStyle(
fontSize: 18,
fontWeight:
FontWeight.bold,
color:
Theme.of(
context,
)
.colorScheme
.primary,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color:
_getFundStatusColor(
context,
fund.status,
).withOpacity(
0.1,
),
borderRadius:
BorderRadius.circular(
12,
),
),
child: Text(
_getFundStatusText(
fund.status,
),
style: TextStyle(
color:
_getFundStatusColor(
context,
fund.status,
),
fontSize: 12,
fontWeight:
FontWeight.w600,
),
),
),
],
),
if (fund.message != null &&
fund
.message!
.isNotEmpty) ...[
const Gap(8),
Text(
fund.message!,
style:
Theme.of(context)
.textTheme
.bodyMedium,
),
],
const Gap(8),
Text(
'${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color:
Theme.of(context)
.colorScheme
.onSurfaceVariant,
_getFundStatusColor(
context,
fund.status,
).withOpacity(0.1),
borderRadius:
BorderRadius.circular(
12,
),
),
child: Text(
_getFundStatusText(
fund.status,
),
style: TextStyle(
color:
_getFundStatusColor(
context,
fund.status,
),
fontSize: 12,
fontWeight:
FontWeight.w600,
),
),
),
],
),
),
if (fund.message != null &&
fund.message!.isNotEmpty) ...[
const Gap(8),
Text(
fund.message!,
style:
Theme.of(
context,
).textTheme.bodyMedium,
),
],
const Gap(8),
Text(
'${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
],
),
);
},
),
loading:
() => const Center(
child: CircularProgressIndicator(),
),
error:
(error, stack) =>
Center(child: Text('Error: $error')),
),
),
),
);
},
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => Center(child: Text('Error: $error')),
),
// Create new fund and return it
SingleChildScrollView(
@@ -314,7 +301,9 @@ class ComposeFundSheet extends HookConsumerWidget {
await Future.delayed(
const Duration(seconds: 1),
);
ref.invalidate(walletFundsProvider);
ref.invalidate(
walletFundsNotifierProvider,
);
// Return the created fund
final updatedResp = await client.get(