import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import '../controls/app_bottom_nav_bar.dart'; import '../controls/async_value_widget.dart'; import '../controls/connection_list_widget.dart'; import '../controls/current_profile_button.dart'; import '../controls/error_message_widget.dart'; import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/search_panel.dart'; import '../controls/search_result_status_control.dart'; import '../controls/standard_app_drawer.dart'; import '../controls/timeline/link_preview_control.dart'; import '../models/auth/profile.dart'; import '../models/networking/paging_data.dart'; import '../models/timeline_entry.dart'; import '../riverpod_controllers/account_services.dart'; import '../riverpod_controllers/connection_manager_services.dart'; import '../riverpod_controllers/entry_tree_item_services.dart'; import '../riverpod_controllers/hashtag_service.dart'; import '../riverpod_controllers/networking/friendica_trending_client_services.dart'; import '../routes.dart'; import '../utils/snackbar_builder.dart'; class ExploreScreen extends ConsumerWidget { const ExploreScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return DefaultTabController( length: 4, child: Scaffold( drawer: const StandardAppDrawer(skipPopDismiss: false), appBar: AppBar( leading: const CurrentProfileButton(), title: const TabBar( tabs: [ Tab(icon: Icon(Icons.search)), Tab(icon: Icon(Icons.bolt)), Tab(icon: Icon(Icons.tag)), Tab(icon: Icon(Icons.recommend)) ], ), ), body: SafeArea( child: TabBarView(children: [ const SearchPanel(), _TrendsPanel(), const _FollowedTagsPanel(), const _SuggestedConnectionsWidget(), ]), ), bottomNavigationBar: const AppBottomNavBar( currentButton: NavBarButtons.explore, ), ), ); } } enum TrendsType { globalTags('Global Tags'), localTags('Local Tags'), statuses('Statuses'), links('Links'), ; final String name; const TrendsType(this.name); } class _TrendsPanel extends ConsumerStatefulWidget { @override ConsumerState<_TrendsPanel> createState() => _TrendsPanelState(); } class _TrendsPanelState extends ConsumerState<_TrendsPanel> { var type = TrendsType.globalTags; @override Widget build(BuildContext context) { ref.watch(activeProfileProvider); final trendingResults = switch (type) { TrendsType.globalTags => const _TrendingTagsWidget( local: false, sortByUses: true, ), TrendsType.localTags => const _TrendingTagsWidget( local: true, sortByUses: true, ), TrendsType.statuses => const _TrendingStatusesWidget(), TrendsType.links => const _TrendingLinksWidget(), }; return SafeArea( child: Stack( children: [ Column( children: [ Text( 'Trending ${type.name}', style: Theme.of(context).textTheme.headlineSmall, ), Expanded( child: ResponsiveMaxWidth(child: trendingResults), ), ], ), Positioned( right: 5, child: PopupMenuButton( onSelected: (newType) => setState(() { type = newType; }), itemBuilder: (context) => TrendsType.values .map((t) => PopupMenuItem( value: t, child: Text(t.name), )) .toList(), ), ) ], )); } } class _TrendingTagsWidget extends ConsumerWidget { static final _logger = Logger('TrendingTagsWidget'); final bool local; final bool sortByUses; const _TrendingTagsWidget({ required this.local, required this.sortByUses, }); @override Widget build(BuildContext context, WidgetRef ref) { _logger.fine('Build'); final profile = ref.watch(activeProfileProvider); return AsyncValueWidget( ref.watch(trendingHashtagsProvider( profile, page: const PagingData(limit: 100), local: local, )), valueBuilder: (vbContext, vbRef, result) => result.fold( onSuccess: (tags) { if (sortByUses) { tags.sort((t1, t2) => -t1.history.uses.compareTo(t2.history.uses)); } return ListView.builder( itemCount: tags.length, itemBuilder: (_, index) { final tag = tags[index]; return ListTile( title: Text(tag.name), subtitle: Text( '${tag.history.accounts} people have mentioned this tag ${tag.history.uses} times'), onTap: () { vbContext.push('${ScreenPaths.tagView}/${tag.name}'); }, ); }); }, onError: (error) => Center( child: Column( children: [ErrorMessageWidget(message: error.message)], ), ), ), ); } } class _TrendingLinksWidget extends ConsumerWidget { static final _logger = Logger('TrendingLinksWidget'); const _TrendingLinksWidget(); @override Widget build(BuildContext context, WidgetRef ref) { _logger.fine('Build'); final profile = ref.watch(activeProfileProvider); return AsyncValueWidget( ref.watch(trendingLinksProvider( profile, page: const PagingData(limit: 100), )), valueBuilder: (vbContext, vbRef, result) => result.fold( onSuccess: (previews) { return ListView.separated( padding: const EdgeInsets.all(10.0), itemCount: previews.length, itemBuilder: (_, index) { return LinkPreviewControl(preview: previews[index]); }, separatorBuilder: (BuildContext context, int index) => const VerticalPadding()); }, onError: (error) => Center( child: Column( children: [ErrorMessageWidget(message: error.message)], ), ), ), ); } } class _TrendingStatusesWidget extends ConsumerWidget { static final _logger = Logger('TrendingLinksWidget'); const _TrendingStatusesWidget(); @override Widget build(BuildContext context, WidgetRef ref) { _logger.fine('Build'); final profile = ref.watch(activeProfileProvider); return AsyncValueWidget( ref.watch(trendingStatusesProvider( profile, page: const PagingData(limit: 50), )), valueBuilder: (vbContext, vbRef, result) => result.fold( onSuccess: (statuses) { return ListView.separated( itemCount: statuses.length, itemBuilder: (_, index) { return buildStatusListTile( context, ref, profile, statuses[index], ); }, separatorBuilder: (BuildContext context, int index) => const Divider(), ); }, onError: (error) => Center( child: Column( children: [ErrorMessageWidget(message: error.message)], ), ), ), ); } Widget buildStatusListTile(BuildContext context, WidgetRef ref, Profile profile, TimelineEntry status) { return SearchResultStatusControl(status, () async { final result = await ref .read(timelineUpdaterProvider(profile).notifier) .refreshStatusChain(status.id); if (context.mounted) { result.match( onSuccess: (entry) => context.push('/post/view/${entry.id}/${status.id}'), onError: (error) => buildSnackbar(context, 'Error getting post: $error')); } }); } } class _FollowedTagsPanel extends ConsumerWidget { const _FollowedTagsPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final profile = ref.watch(activeProfileProvider); return SafeArea( child: Column(children: [ Text( 'Followed Tags', style: Theme.of(context).textTheme.headlineSmall, ), Expanded( child: ResponsiveMaxWidth( child: AsyncValueWidget( ref.watch(followedTagsMapProvider(profile)), valueBuilder: (vbContext, vbRef, result) => result.fold( onSuccess: (followedTags) { final tags = followedTags.keys.toList(); return ListView.builder( itemCount: tags.length, itemBuilder: (_, index) { final tag = tags[index]; return ListTile( title: Text('#$tag'), onTap: () { vbContext.push('${ScreenPaths.tagView}/$tag'); }, ); }); }, onError: (error) => Center( child: Column( children: [ErrorMessageWidget(message: error.message)], ), ), ), )), ), ])); } } class _SuggestedConnectionsWidget extends ConsumerWidget { static final _logger = Logger('SuggestedFollowersWidget'); const _SuggestedConnectionsWidget(); @override Widget build(BuildContext context, WidgetRef ref) { _logger.fine('Build'); final profile = ref.watch(activeProfileProvider); return AsyncValueWidget( ref.watch(suggestedConnectionsProvider(profile)), valueBuilder: (vbContext, vbRef, result) => result.fold( onSuccess: (connections) { return ListView.builder( itemCount: connections.length, itemBuilder: (_, index) { return ConnectionListWidget(contactId: connections[index].id); }); }, onError: (error) => Center( child: Column( children: [ErrorMessageWidget(message: error.message)], ), ), ), ); } }