2024-12-21 05:08:29 +00:00
|
|
|
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';
|
2024-12-23 20:34:43 +00:00
|
|
|
import '../controls/async_value_widget.dart';
|
2024-12-24 17:21:44 +00:00
|
|
|
import '../controls/connection_list_widget.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
import '../controls/current_profile_button.dart';
|
2024-12-23 20:34:43 +00:00
|
|
|
import '../controls/error_message_widget.dart';
|
|
|
|
import '../controls/padding.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
import '../controls/responsive_max_width.dart';
|
2024-12-23 20:34:43 +00:00
|
|
|
import '../controls/search_panel.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
import '../controls/search_result_status_control.dart';
|
|
|
|
import '../controls/standard_app_drawer.dart';
|
2024-12-23 20:34:43 +00:00
|
|
|
import '../controls/timeline/link_preview_control.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
import '../models/auth/profile.dart';
|
|
|
|
import '../models/networking/paging_data.dart';
|
|
|
|
import '../models/timeline_entry.dart';
|
|
|
|
import '../riverpod_controllers/account_services.dart';
|
2024-12-24 17:21:44 +00:00
|
|
|
import '../riverpod_controllers/connection_manager_services.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
import '../riverpod_controllers/entry_tree_item_services.dart';
|
2024-12-23 20:47:14 +00:00
|
|
|
import '../riverpod_controllers/hashtag_service.dart';
|
2024-12-22 18:07:41 +00:00
|
|
|
import '../riverpod_controllers/networking/friendica_trending_client_services.dart';
|
2024-12-21 05:08:29 +00:00
|
|
|
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(
|
2024-12-24 17:21:44 +00:00
|
|
|
length: 4,
|
2024-12-21 05:08:29 +00:00
|
|
|
child: Scaffold(
|
|
|
|
drawer: const StandardAppDrawer(skipPopDismiss: false),
|
|
|
|
appBar: AppBar(
|
|
|
|
leading: const CurrentProfileButton(),
|
2024-12-23 20:34:43 +00:00
|
|
|
title: const TabBar(
|
2024-12-23 20:47:14 +00:00
|
|
|
tabs: [
|
|
|
|
Tab(icon: Icon(Icons.search)),
|
|
|
|
Tab(icon: Icon(Icons.bolt)),
|
|
|
|
Tab(icon: Icon(Icons.tag)),
|
2024-12-24 17:21:44 +00:00
|
|
|
Tab(icon: Icon(Icons.recommend))
|
2024-12-23 20:47:14 +00:00
|
|
|
],
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
body: SafeArea(
|
2024-12-22 18:07:41 +00:00
|
|
|
child: TabBarView(children: [
|
2024-12-23 20:34:43 +00:00
|
|
|
const SearchPanel(),
|
2024-12-22 18:07:41 +00:00
|
|
|
_TrendsPanel(),
|
2024-12-23 20:47:14 +00:00
|
|
|
const _FollowedTagsPanel(),
|
2024-12-24 17:21:44 +00:00
|
|
|
const _SuggestedConnectionsWidget(),
|
2024-12-22 18:07:41 +00:00
|
|
|
]),
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
|
|
|
bottomNavigationBar: const AppBottomNavBar(
|
2024-12-23 20:34:43 +00:00
|
|
|
currentButton: NavBarButtons.explore,
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-22 18:07:41 +00:00
|
|
|
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) {
|
2024-12-23 20:34:43 +00:00
|
|
|
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(),
|
|
|
|
};
|
2024-12-22 18:07:41 +00:00
|
|
|
|
|
|
|
return SafeArea(
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Column(
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
'Trending ${type.name}',
|
|
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
|
|
),
|
|
|
|
Expanded(
|
2024-12-23 20:34:43 +00:00
|
|
|
child: ResponsiveMaxWidth(child: trendingResults),
|
2024-12-22 18:07:41 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
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 {
|
2024-12-23 20:34:43 +00:00
|
|
|
static final _logger = Logger('TrendingTagsWidget');
|
2024-12-22 18:07:41 +00:00
|
|
|
final bool local;
|
|
|
|
final bool sortByUses;
|
|
|
|
|
|
|
|
const _TrendingTagsWidget({
|
|
|
|
required this.local,
|
|
|
|
required this.sortByUses,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2024-12-23 20:34:43 +00:00
|
|
|
_logger.fine('Build');
|
2024-12-22 18:07:41 +00:00
|
|
|
final profile = ref.watch(activeProfileProvider);
|
|
|
|
return AsyncValueWidget(
|
2024-12-23 20:34:43 +00:00
|
|
|
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)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
2024-12-22 18:07:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
class _TrendingLinksWidget extends ConsumerWidget {
|
|
|
|
static final _logger = Logger('TrendingLinksWidget');
|
2024-12-21 05:08:29 +00:00
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
const _TrendingLinksWidget();
|
2024-12-21 05:08:29 +00:00
|
|
|
|
|
|
|
@override
|
2024-12-23 20:34:43 +00:00
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
_logger.fine('Build');
|
2024-12-21 05:08:29 +00:00
|
|
|
final profile = ref.watch(activeProfileProvider);
|
2024-12-23 20:34:43 +00:00
|
|
|
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)],
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
2024-12-23 20:34:43 +00:00
|
|
|
),
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-12-23 20:34:43 +00:00
|
|
|
}
|
2024-12-21 05:08:29 +00:00
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
class _TrendingStatusesWidget extends ConsumerWidget {
|
|
|
|
static final _logger = Logger('TrendingLinksWidget');
|
2024-12-21 05:08:29 +00:00
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
const _TrendingStatusesWidget();
|
2024-12-21 05:08:29 +00:00
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
@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],
|
|
|
|
);
|
2024-12-21 05:08:29 +00:00
|
|
|
},
|
2024-12-23 20:34:43 +00:00
|
|
|
separatorBuilder: (BuildContext context, int index) =>
|
|
|
|
const Divider(),
|
2024-12-21 05:08:29 +00:00
|
|
|
);
|
2024-12-23 20:34:43 +00:00
|
|
|
},
|
|
|
|
onError: (error) => Center(
|
|
|
|
child: Column(
|
|
|
|
children: [ErrorMessageWidget(message: error.message)],
|
|
|
|
),
|
|
|
|
),
|
2024-12-21 05:08:29 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-12-23 20:34:43 +00:00
|
|
|
Widget buildStatusListTile(BuildContext context, WidgetRef ref,
|
|
|
|
Profile profile, TimelineEntry status) {
|
2024-12-21 05:08:29 +00:00
|
|
|
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'));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-12-23 20:47:14 +00:00
|
|
|
|
|
|
|
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)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)),
|
|
|
|
),
|
|
|
|
]));
|
|
|
|
}
|
|
|
|
}
|
2024-12-24 17:21:44 +00:00
|
|
|
|
|
|
|
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)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|