320 lines
9.5 KiB
Dart
320 lines
9.5 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:gap/gap.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:island/models/activity.dart';
|
|
import 'package:island/pods/userinfo.dart';
|
|
import 'package:island/route.gr.dart';
|
|
import 'package:island/services/responsive.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:island/screens/tabs.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';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
|
|
part 'explore.g.dart';
|
|
|
|
@RoutePage()
|
|
class ExploreShellScreen extends ConsumerWidget {
|
|
const ExploreShellScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final isWide = isWideScreen(context);
|
|
|
|
if (isWide) {
|
|
return AppBackground(
|
|
isRoot: true,
|
|
child: Row(
|
|
children: [
|
|
Flexible(flex: 2, child: ExploreScreen(isAside: true)),
|
|
VerticalDivider(width: 1),
|
|
Flexible(flex: 3, child: AutoRouter()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return AppBackground(isRoot: true, child: AutoRouter());
|
|
}
|
|
}
|
|
|
|
@RoutePage()
|
|
class ExploreScreen extends HookConsumerWidget {
|
|
final bool isAside;
|
|
const ExploreScreen({super.key, this.isAside = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final isWide = isWideScreen(context);
|
|
if (isWide && !isAside) {
|
|
return const EmptyPageHolder();
|
|
}
|
|
|
|
final tabController = useTabController(initialLength: 3);
|
|
final currentFilter = useState<String?>(null);
|
|
|
|
useEffect(() {
|
|
void listener() {
|
|
switch (tabController.index) {
|
|
case 0:
|
|
currentFilter.value = null;
|
|
break;
|
|
case 1:
|
|
currentFilter.value = 'subscriptions';
|
|
break;
|
|
case 2:
|
|
currentFilter.value = 'friends';
|
|
break;
|
|
}
|
|
}
|
|
|
|
tabController.addListener(listener);
|
|
return () => tabController.removeListener(listener);
|
|
}, [tabController]);
|
|
|
|
final activitiesNotifier = ref.watch(
|
|
activityListNotifierProvider(currentFilter.value).notifier,
|
|
);
|
|
|
|
return TourTriggerWidget(
|
|
child: AppScaffold(
|
|
extendBody: false, // Prevent conflicts with tabs navigation
|
|
appBar: AppBar(
|
|
toolbarHeight: 0,
|
|
bottom: TabBar(
|
|
controller: tabController,
|
|
tabs: [
|
|
Tab(
|
|
child: Text(
|
|
'explore'.tr(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
Tab(
|
|
child: Text(
|
|
'exploreFilterSubscriptions'.tr(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
Tab(
|
|
child: Text(
|
|
'exploreFilterFriends'.tr(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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: TabbedFabLocation(context),
|
|
body: TabBarView(
|
|
controller: tabController,
|
|
children: [
|
|
_buildActivityList(ref, null),
|
|
_buildActivityList(ref, 'subscriptions'),
|
|
_buildActivityList(ref, 'friends'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActivityList(WidgetRef ref, String? filter) {
|
|
final activitiesNotifier = ref.watch(
|
|
activityListNotifierProvider(filter).notifier,
|
|
);
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
|
child: PagingHelperView(
|
|
provider: activityListNotifierProvider(filter),
|
|
futureRefreshable: activityListNotifierProvider(filter).future,
|
|
notifierRefreshable: activityListNotifierProvider(filter).notifier,
|
|
contentBuilder:
|
|
(data, widgetCount, endItemView) => Center(
|
|
child: _ActivityListView(
|
|
data: data,
|
|
widgetCount: widgetCount,
|
|
endItemView: endItemView,
|
|
activitiesNotifier: activitiesNotifier,
|
|
contentOnly: filter != null,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ActivityListView extends HookConsumerWidget {
|
|
final CursorPagingData<SnActivity> data;
|
|
final int widgetCount;
|
|
final Widget endItemView;
|
|
final bool contentOnly;
|
|
final ActivityListNotifier activitiesNotifier;
|
|
|
|
const _ActivityListView({
|
|
required this.data,
|
|
required this.widgetCount,
|
|
required this.endItemView,
|
|
required this.activitiesNotifier,
|
|
this.contentOnly = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final user = ref.watch(userInfoProvider);
|
|
|
|
return CustomScrollView(
|
|
slivers: [
|
|
if (user.hasValue && !contentOnly)
|
|
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':
|
|
case 'posts.new.replies':
|
|
final isReply = item.type == 'posts.new.replies';
|
|
itemWidget = PostItem(
|
|
backgroundColor:
|
|
isWideScreen(context) ? Colors.transparent : null,
|
|
item: SnPost.fromJson(item.data),
|
|
padding:
|
|
isReply
|
|
? EdgeInsets.only(left: 16, right: 16, bottom: 16)
|
|
: null,
|
|
onRefresh: (_) {
|
|
activitiesNotifier.forceRefresh();
|
|
},
|
|
onUpdate: (post) {
|
|
activitiesNotifier.updateOne(
|
|
index,
|
|
item.copyWith(data: post.toJson()),
|
|
);
|
|
},
|
|
);
|
|
if (isReply) {
|
|
itemWidget = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Symbols.reply),
|
|
const Gap(8),
|
|
Text('Replying your post'),
|
|
],
|
|
).padding(horizontal: 20, vertical: 8),
|
|
itemWidget,
|
|
],
|
|
);
|
|
}
|
|
break;
|
|
default:
|
|
itemWidget = const Placeholder();
|
|
}
|
|
|
|
return Column(children: [itemWidget, const Divider(height: 1)]);
|
|
},
|
|
),
|
|
SliverGap(getTabbedPadding(context).bottom),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
@riverpod
|
|
class ActivityListNotifier extends _$ActivityListNotifier
|
|
with CursorPagingNotifierMixin<SnActivity> {
|
|
@override
|
|
Future<CursorPagingData<SnActivity>> build(String? filter) =>
|
|
fetch(cursor: null);
|
|
|
|
@override
|
|
Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async {
|
|
final client = ref.read(apiClientProvider);
|
|
final take = 20;
|
|
|
|
final queryParameters = {
|
|
if (cursor != null) 'cursor': cursor,
|
|
'take': take,
|
|
if (filter != null) 'filter': filter,
|
|
};
|
|
|
|
final response = await client.get(
|
|
'/activities',
|
|
queryParameters: queryParameters,
|
|
);
|
|
|
|
final List<SnActivity> items =
|
|
(response.data as List)
|
|
.map((e) => SnActivity.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
|
|
final nextCursor =
|
|
items
|
|
.map((x) => x.createdAt)
|
|
.lastOrNull
|
|
?.toUtc()
|
|
.toIso8601String()
|
|
.toString();
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
}
|