kopia lustrzana https://gitlab.com/mysocialportal/relatica
313 wiersze
9.7 KiB
Dart
313 wiersze
9.7 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 '../models/auth/profile.dart';
|
|
import '../models/connection.dart';
|
|
import '../models/search_results.dart';
|
|
import '../models/search_types.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/search_result_services.dart';
|
|
import '../routes.dart';
|
|
import '../utils/snackbar_builder.dart';
|
|
import 'async_value_widget.dart';
|
|
import 'error_message_widget.dart';
|
|
import 'image_control.dart';
|
|
import 'responsive_max_width.dart';
|
|
import 'search_result_status_control.dart';
|
|
|
|
class SearchPanel extends ConsumerStatefulWidget {
|
|
final String initialSearchText;
|
|
final SearchTypes initialType;
|
|
final bool showSearchbar;
|
|
|
|
const SearchPanel({
|
|
super.key,
|
|
this.initialSearchText = '',
|
|
this.initialType = SearchTypes.statusesText,
|
|
this.showSearchbar = true,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<SearchPanel> createState() => SearchPanelState();
|
|
}
|
|
|
|
class SearchPanelState extends ConsumerState<SearchPanel> {
|
|
static final _logger = Logger('SearchPanel');
|
|
var searchTextController = TextEditingController();
|
|
var searchType = SearchTypes.statusesText;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
searchTextController.text = widget.initialSearchText;
|
|
searchType = widget.initialType;
|
|
}
|
|
|
|
clearSearchResults() {
|
|
setState(() {
|
|
searchTextController.text = '';
|
|
});
|
|
}
|
|
|
|
Future<void> updateSearchResults(Profile profile, bool reset) async {
|
|
setState(() {});
|
|
await ref
|
|
.read(searchResultsManagerProvider(
|
|
profile, searchType, searchTextController.text)
|
|
.notifier)
|
|
.updateSearchResults(reset: reset);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
_logger.finer('Build');
|
|
final profile = ref.watch(activeProfileProvider);
|
|
final searchResult = ref.watch(searchResultsManagerProvider(
|
|
profile, searchType, searchTextController.text));
|
|
_logger.finer('Search Result = $searchResult');
|
|
final searching = switch (searchResult) {
|
|
AsyncData() => false,
|
|
AsyncError() => false,
|
|
_ => true
|
|
};
|
|
|
|
final body = AsyncValueWidget(searchResult,
|
|
valueBuilder: (_, __, searchResult) => searchResult.fold(
|
|
onSuccess: (results) =>
|
|
ResponsiveMaxWidth(child: buildResultBody(profile, results)),
|
|
onError: (error) => ErrorMessageWidget(message: error.message)));
|
|
|
|
final searchbar = Row(
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: TextField(
|
|
controller: searchTextController,
|
|
onSubmitted: (value) {
|
|
searchTextController.text = value;
|
|
updateSearchResults(profile, true);
|
|
},
|
|
onTapOutside: (event) {
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
},
|
|
decoration: InputDecoration(
|
|
labelText: searchType == SearchTypes.directLink
|
|
? 'URL'
|
|
: '${searchType.toLabel()} Search Text',
|
|
alignLabelWithHint: true,
|
|
suffixIcon: IconButton(
|
|
onPressed: clearSearchResults,
|
|
icon: const Icon(Icons.clear),
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
),
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
updateSearchResults(profile, true);
|
|
},
|
|
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()),
|
|
],
|
|
);
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
if (searching) {
|
|
return;
|
|
}
|
|
|
|
await updateSearchResults(profile, true);
|
|
},
|
|
child: Column(
|
|
children: [
|
|
if (widget.showSearchbar) searchbar,
|
|
Expanded(child: body),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildResultBody(Profile profile, SearchResults searchResult) {
|
|
_logger.finer('Building search result body with: $searchResult');
|
|
switch (searchType) {
|
|
case SearchTypes.hashTag:
|
|
return buildHashtagResultWidget(profile, searchResult);
|
|
case SearchTypes.account:
|
|
return buildAccountResultWidget(profile, searchResult);
|
|
case SearchTypes.statusesText:
|
|
return buildStatusResultWidget(profile, searchResult);
|
|
case SearchTypes.directLink:
|
|
return buildDirectLinkResult(profile, searchResult);
|
|
}
|
|
}
|
|
|
|
Widget buildHashtagResultWidget(Profile profile, SearchResults searchResult) {
|
|
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, false);
|
|
},
|
|
child: const Text('Load more results'),
|
|
);
|
|
}
|
|
return ListTile(
|
|
title: Text(hashtags[index]),
|
|
onTap: () => context.push(
|
|
'${ScreenPaths.tagView}/${hashtags[index]}',
|
|
),
|
|
);
|
|
},
|
|
itemCount: hashtags.length + 1,
|
|
);
|
|
}
|
|
|
|
Widget buildAccountResultWidget(Profile profile, SearchResults searchResult) {
|
|
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, false);
|
|
},
|
|
child: const Text('Load more results'),
|
|
);
|
|
}
|
|
|
|
return buildConnectionListTile(profile, accounts[index]);
|
|
},
|
|
itemCount: accounts.length + 1,
|
|
);
|
|
}
|
|
|
|
Widget buildStatusResultWidget(Profile profile, SearchResults searchResult) {
|
|
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, false);
|
|
},
|
|
child: const Text('Load more results'),
|
|
);
|
|
}
|
|
return buildStatusListTile(profile, statuses[index]);
|
|
},
|
|
itemCount: statuses.length + 1,
|
|
);
|
|
}
|
|
|
|
Widget buildDirectLinkResult(Profile profile, SearchResults searchResult) {
|
|
if (searchResult.isEmpty) {
|
|
return buildEmptyResult();
|
|
}
|
|
|
|
return ListView(physics: const AlwaysScrollableScrollPhysics(), children: [
|
|
if (searchResult.statuses.isNotEmpty)
|
|
buildStatusListTile(profile, searchResult.statuses.first),
|
|
if (searchResult.accounts.isNotEmpty)
|
|
buildConnectionListTile(profile, searchResult.accounts.first),
|
|
]);
|
|
}
|
|
|
|
Widget buildEmptyResult() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(searchTextController.text.isEmpty
|
|
? 'Type search text to search'
|
|
: 'No results for ${searchType.toLabel()} search on: ${searchTextController.text}'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildConnectionListTile(Profile profile, Connection connection) {
|
|
return ListTile(
|
|
onTap: () async {
|
|
buildSnackbar(
|
|
context, 'Fetching profile data to open ${connection.name}');
|
|
await ref
|
|
.read(connectionByIdProvider(profile, connection.id))
|
|
.withErrorAsync((_) => ref
|
|
.read(connectionModifierProvider(profile, connection).notifier)
|
|
.fullRefresh());
|
|
|
|
if (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(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'));
|
|
}
|
|
});
|
|
}
|
|
}
|