import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gap/gap.dart'; import 'package:island/models/activity.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/route.gr.dart'; import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; import 'package:island/widgets/check_in.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/tour/tour.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/network.dart'; part 'explore.g.dart'; @RoutePage() class ExploreScreen extends ConsumerWidget { const ExploreScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userInfoProvider); final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier); return TourTriggerWidget( child: AppScaffold( appBar: AppBar(title: const Text('explore').tr()), floatingActionButton: FloatingActionButton( heroTag: Key("explore-page-fab"), onPressed: () { context.router.push(PostComposeRoute()).then((value) { if (value != null) { activitiesNotifier.forceRefresh(); } }); }, child: const Icon(Symbols.edit), ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, body: RefreshIndicator( onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), child: PagingHelperView( provider: activityListNotifierProvider, futureRefreshable: activityListNotifierProvider.future, notifierRefreshable: activityListNotifierProvider.notifier, contentBuilder: (data, widgetCount, endItemView) => CustomScrollView( slivers: [ if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()), SliverList.builder( itemCount: widgetCount, itemBuilder: (context, index) { if (index == widgetCount - 1) { return endItemView; } final item = data.items[index]; if (item.data == null) return const SizedBox.shrink(); Widget itemWidget; switch (item.type) { case 'posts.new': itemWidget = PostItem( item: SnPost.fromJson(item.data), onRefresh: (_) { activitiesNotifier.forceRefresh(); }, onUpdate: (post) { activitiesNotifier.updateOne( index, item.copyWith(data: post.toJson()), ); }, ); break; case 'accounts.check-in': itemWidget = CheckInActivityWidget(item: item); break; case 'accounts.status': itemWidget = StatusActivityWidget(item: item); break; default: itemWidget = const Placeholder(); } return Column( children: [itemWidget, const Divider(height: 1)], ); }, ), SliverGap(MediaQuery.of(context).padding.bottom + 16), ], ), ), ), ), ); } } @riverpod class ActivityListNotifier extends _$ActivityListNotifier with CursorPagingNotifierMixin { @override Future> build() => fetch(cursor: null); @override Future> fetch({required String? cursor}) async { final client = ref.read(apiClientProvider); final offset = cursor == null ? 0 : int.parse(cursor); final take = 20; final response = await client.get( '/activities', queryParameters: {'offset': offset, 'take': take}, ); final List items = (response.data as List) .map((e) => SnActivity.fromJson(e as Map)) .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; return CursorPagingData( items: items, hasMore: hasMore, nextCursor: nextCursor, ); } void updateOne(int index, SnActivity activity) { final currentState = state.valueOrNull; if (currentState == null) return; final updatedItems = [...currentState.items]; updatedItems[index] = activity; state = AsyncData( CursorPagingData( items: updatedItems, hasMore: currentState.hasMore, nextCursor: currentState.nextCursor, ), ); } }