relatica/lib/screens/search_screen.dart

346 wiersze
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/current_profile_button.dart';
import '../controls/image_control.dart';
import '../controls/responsive_max_width.dart';
import '../controls/search_result_status_control.dart';
import '../controls/standard_app_drawer.dart';
import '../friendica_client/friendica_client.dart';
import '../friendica_client/paging_data.dart';
import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/connection.dart';
import '../models/search_results.dart';
import '../models/search_types.dart';
import '../models/timeline_entry.dart';
import '../routes.dart';
import '../services/auth_service.dart';
import '../services/connections_manager.dart';
import '../services/entry_manager_service.dart';
import '../services/network_status_service.dart';
import '../utils/active_profile_selector.dart';
import '../utils/snackbar_builder.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
static const limit = 50;
static final _logger = Logger('$SearchScreen');
var searchText = '';
var searchType = SearchTypes.statusesText;
var searching = false;
PagingData nextPage = PagingData(limit: limit);
var searchResult = SearchResults.empty();
PagingData genNextPageData() {
late final int offset;
switch (searchType) {
case SearchTypes.hashTag:
offset = searchResult.hashtags.length;
break;
case SearchTypes.account:
offset = searchResult.accounts.length;
break;
case SearchTypes.statusesText:
offset = searchResult.statuses.length;
break;
case SearchTypes.directLink:
offset = 0;
break;
}
return PagingData(limit: limit, offset: offset);
}
Future<void> updateSearchResults(Profile profile, {bool reset = true}) async {
print('Starting update');
if (reset) {
nextPage = PagingData(limit: limit);
searchResult = SearchResults.empty();
}
setState(() {
searching = true;
});
print('Search $searchType on $searchText');
final result =
await SearchClient(profile).search(searchType, searchText, nextPage);
result.match(
onSuccess: (result) {
searchResult = reset ? result.data : searchResult.merge(result.data);
nextPage = result.next ?? genNextPageData();
},
onError: (error) =>
buildSnackbar(context, 'Error getting search result: $error'),
);
setState(() {
searching = false;
});
print('Ending update');
}
@override
Widget build(BuildContext context) {
_logger.info('Build');
final nss = getIt<NetworkStatusService>();
final profileService = context.watch<AccountsService>();
final profile = profileService.currentProfile;
late Widget body;
if (searchResult.isEmpty && searching) {
body = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Searching for ${searchType.toLabel()} on: $searchText',
),
],
),
);
} else {
body = ResponsiveMaxWidth(child: buildResultBody(profile));
}
return Scaffold(
drawer: const StandardAppDrawer(skipPopDismiss: true),
body: SafeArea(
child: RefreshIndicator(
onRefresh: () async {
if (nss.searchLoadingStatus.value) {
return;
}
updateSearchResults(profile);
return;
},
child: Column(
children: [
Row(
children: [
SizedBox(
width: 50.0, child: buildCurrentProfileButton(context)!),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) {
searchText = value;
},
onSubmitted: (value) {
searchText = value;
updateSearchResults(profile);
},
onTapOutside: (event) {
FocusManager.instance.primaryFocus?.unfocus();
},
decoration: InputDecoration(
labelText: searchType == SearchTypes.directLink
? 'URL'
: '${searchType.toLabel()} Search Text',
alignLabelWithHint: true,
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
),
borderRadius: BorderRadius.circular(5.0),
),
),
),
),
),
IconButton(
onPressed: () {
updateSearchResults(profile);
},
icon: const Icon(Icons.search),
),
PopupMenuButton<SearchTypes>(
initialValue: searchType,
onSelected: (type) {
setState(() {
searchType = type;
});
},
itemBuilder: (_) => SearchTypes.values
.map((e) => PopupMenuItem(
value: e,
child: Text(
e.toLabel(),
)))
.toList()),
],
),
if (searching) const LinearProgressIndicator(),
Expanded(child: body),
],
),
),
),
bottomNavigationBar: const AppBottomNavBar(
currentButton: NavBarButtons.search,
),
);
}
Widget buildResultBody(Profile profile) {
_logger.fine('Building search result body with: $searchResult');
switch (searchType) {
case SearchTypes.hashTag:
return buildHashtagResultWidget(profile);
case SearchTypes.account:
return buildAccountResultWidget(profile);
case SearchTypes.statusesText:
return buildStatusResultWidget(profile);
case SearchTypes.directLink:
return buildDirectLinkResult(profile);
}
}
Widget buildHashtagResultWidget(Profile profile) {
final hashtags = searchResult.hashtags;
if (hashtags.isEmpty) {
return buildEmptyResult();
}
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == hashtags.length) {
return TextButton(
onPressed: () {
updateSearchResults(profile, reset: false);
},
child: const Text('Load more results'),
);
}
return ListTile(
title: Text(hashtags[index]),
);
},
itemCount: hashtags.length + 1,
);
}
Widget buildAccountResultWidget(Profile profile) {
final accounts = searchResult.accounts;
if (accounts.isEmpty) {
return buildEmptyResult();
}
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (_, index) {
if (index == accounts.length) {
return TextButton(
onPressed: () {
updateSearchResults(profile, reset: false);
},
child: const Text('Load more results'),
);
}
return buildConnectionListTile(accounts[index]);
},
itemCount: accounts.length + 1,
);
}
Widget buildStatusResultWidget(Profile profile) {
final statuses = searchResult.statuses;
if (statuses.isEmpty) {
return buildEmptyResult();
}
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == statuses.length) {
return TextButton(
onPressed: () {
updateSearchResults(profile, reset: false);
},
child: const Text('Load more results'),
);
}
return buildStatusListTile(statuses[index]);
},
itemCount: statuses.length + 1,
);
}
Widget buildDirectLinkResult(Profile profile) {
if (searchResult.isEmpty) {
return buildEmptyResult();
}
return ListView(physics: const AlwaysScrollableScrollPhysics(), children: [
if (searchResult.statuses.isNotEmpty)
buildStatusListTile(searchResult.statuses.first),
if (searchResult.accounts.isNotEmpty)
buildConnectionListTile(searchResult.accounts.first),
]);
}
Widget buildEmptyResult() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(searchText.isEmpty
? 'Type search text to search'
: 'No results for ${searchType.toLabel()} search on: $searchText'),
],
),
);
}
Widget buildConnectionListTile(Connection connection) {
return ListTile(
onTap: () async {
await getIt<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry
.andThenSuccessAsync((cm) async {
final existingData = cm.getById(connection.id);
if (existingData.isFailure) {
await cm.fullRefresh(connection);
}
});
if (context.mounted) {
context.pushNamed(ScreenPaths.userProfile,
pathParameters: {'id': connection.id});
}
},
leading: ImageControl(
imageUrl: connection.avatarUrl.toString(),
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: () => context.pushNamed(ScreenPaths.userProfile,
pathParameters: {'id': connection.id}),
),
title: Text('${connection.name} (${connection.handle})'),
);
}
Widget buildStatusListTile(TimelineEntry status) {
return SearchResultStatusControl(status, () async {
final result = await getIt<ActiveProfileSelector<EntryManagerService>>()
.activeEntry
.andThenAsync((em) async => em.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'));
}
});
}
}