relatica/lib/screens/explore_screen.dart

344 wiersze
10 KiB
Dart

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<TrendsType>(
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)],
),
),
),
);
}
}