kopia lustrzana https://gitlab.com/mysocialportal/relatica
Initial cut of the search page
rodzic
fb24bc584f
commit
7be5176126
|
@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
|
|||
import '../routes.dart';
|
||||
import '../services/notifications_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
enum NavBarButtons {
|
||||
timelines,
|
||||
|
@ -51,7 +50,7 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
context.pushNamed(ScreenPaths.contacts);
|
||||
break;
|
||||
case NavBarButtons.search:
|
||||
buildSnackbar(context, 'Search screen coming soon...');
|
||||
context.pushNamed(ScreenPaths.search);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../utils/clipboard_utils.dart';
|
||||
import '../utils/url_opening_utils.dart';
|
||||
import 'media_attachment_viewer_control.dart';
|
||||
import 'padding.dart';
|
||||
import 'timeline/link_preview_control.dart';
|
||||
import 'timeline/status_header_control.dart';
|
||||
|
||||
class SearchResultStatusControl extends StatefulWidget {
|
||||
static final _logger = Logger('$SearchResultStatusControl');
|
||||
final TimelineEntry status;
|
||||
|
||||
final Future Function() goToPostFunction;
|
||||
|
||||
const SearchResultStatusControl(this.status, this.goToPostFunction,
|
||||
{super.key});
|
||||
|
||||
@override
|
||||
State<SearchResultStatusControl> createState() =>
|
||||
_SearchResultStatusControlState();
|
||||
}
|
||||
|
||||
class _SearchResultStatusControlState extends State<SearchResultStatusControl> {
|
||||
var showContent = false;
|
||||
|
||||
TimelineEntry get status => widget.status;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
showContent = widget.status.spoilerText.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SearchResultStatusControl._logger
|
||||
.finest('Building ${widget.status.toShortString()}');
|
||||
const otherPadding = 8.0;
|
||||
final body = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
border: Border.all(width: 0.5),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).dividerColor,
|
||||
blurRadius: 2,
|
||||
offset: Offset(4, 4),
|
||||
spreadRadius: 0.1,
|
||||
blurStyle: BlurStyle.normal,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatusHeaderControl(
|
||||
entry: widget.status,
|
||||
showIsCommentText: true,
|
||||
),
|
||||
),
|
||||
buildMenuControl(context),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (status.spoilerText.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showContent = !showContent;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Content Summary: ${status.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
||||
if (showContent) ...[
|
||||
buildBody(context),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (status.linkPreviewData != null)
|
||||
LinkPreviewControl(preview: status.linkPreviewData!),
|
||||
buildMediaBar(context),
|
||||
],
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: otherPadding,
|
||||
right: otherPadding,
|
||||
top: otherPadding,
|
||||
bottom: otherPadding,
|
||||
),
|
||||
child: body,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return HtmlWidget(
|
||||
widget.status.body,
|
||||
onTapUrl: (url) async {
|
||||
return await openUrlStringInSystembrowser(context, url, 'link');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMediaBar(BuildContext context) {
|
||||
final items = widget.status.mediaAttachments;
|
||||
if (items.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 250.0,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
return MediaAttachmentViewerControl(
|
||||
attachments: items,
|
||||
index: index,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return HorizontalPadding();
|
||||
},
|
||||
itemCount: items.length));
|
||||
}
|
||||
|
||||
Widget buildMenuControl(BuildContext context) {
|
||||
const goToPost = 'Open Post';
|
||||
const copyText = 'Copy Post Text';
|
||||
const copyUrl = 'Copy URL';
|
||||
const openExternal = 'Open In Browser';
|
||||
final options = [
|
||||
goToPost,
|
||||
copyText,
|
||||
openExternal,
|
||||
copyUrl,
|
||||
];
|
||||
|
||||
return PopupMenuButton<String>(onSelected: (menuOption) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (menuOption) {
|
||||
case goToPost:
|
||||
await widget.goToPostFunction();
|
||||
break;
|
||||
case openExternal:
|
||||
await openUrlStringInSystembrowser(
|
||||
context,
|
||||
widget.status.externalLink,
|
||||
'Status',
|
||||
);
|
||||
break;
|
||||
case copyUrl:
|
||||
await copyToClipboard(
|
||||
context: context,
|
||||
text: widget.status.externalLink,
|
||||
message: 'Status link copied to clipboard',
|
||||
);
|
||||
break;
|
||||
case copyText:
|
||||
await copyToClipboard(
|
||||
context: context,
|
||||
text: widget.status.body,
|
||||
message: 'Status text copied to clipboard',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
//do nothing
|
||||
}
|
||||
}, itemBuilder: (context) {
|
||||
return options
|
||||
.map((o) => PopupMenuItem(value: o, child: Text(o)))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -16,10 +16,12 @@ import '../padding.dart';
|
|||
class StatusHeaderControl extends StatelessWidget {
|
||||
static final _logger = Logger('$StatusHeaderControl');
|
||||
final TimelineEntry entry;
|
||||
final bool showIsCommentText;
|
||||
|
||||
const StatusHeaderControl({
|
||||
super.key,
|
||||
required this.entry,
|
||||
this.showIsCommentText = false,
|
||||
});
|
||||
|
||||
void goToProfile(BuildContext context, String id) {
|
||||
|
@ -97,6 +99,11 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
],
|
||||
if (showIsCommentText && entry.parentId.isNotEmpty)
|
||||
Text(
|
||||
' ...made a comment:',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'services/connections_manager.dart';
|
|||
import 'services/direct_message_service.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/feature_version_checker.dart';
|
||||
import 'services/fediverse_server_validator.dart';
|
||||
import 'services/follow_requests_manager.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/hashtag_service.dart';
|
||||
|
@ -36,6 +37,8 @@ Future<void> dependencyInjectionInitialization() async {
|
|||
getIt.registerSingleton<IHashtagRepo>(ObjectBoxHashtagRepo());
|
||||
getIt.registerSingleton<HashtagService>(HashtagService());
|
||||
getIt.registerSingleton<NetworkStatusService>(NetworkStatusService());
|
||||
getIt.registerSingleton<FediverseServiceValidator>(
|
||||
FediverseServiceValidator());
|
||||
getIt.registerSingleton<FriendicaVersionChecker>(
|
||||
const FriendicaVersionChecker());
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ import '../models/group_data.dart';
|
|||
import '../models/image_entry.dart';
|
||||
import '../models/instance_info.dart';
|
||||
import '../models/media_attachment_uploads/image_types_enum.dart';
|
||||
import '../models/search_results.dart';
|
||||
import '../models/search_types.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/user_notification.dart';
|
||||
import '../models/visibility.dart';
|
||||
|
@ -31,6 +33,7 @@ import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
|
|||
import '../serializers/mastodon/group_data_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/notification_mastodon_extension.dart';
|
||||
import '../serializers/mastodon/search_result_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
|
@ -778,6 +781,29 @@ class StatusesClient extends FriendicaClient {
|
|||
}
|
||||
}
|
||||
|
||||
class SearchClient extends FriendicaClient {
|
||||
static final _logger = Logger('$StatusesClient');
|
||||
|
||||
SearchClient(super.credentials) : super();
|
||||
|
||||
FutureResult<PagedResponse<SearchResults>, ExecError> search(
|
||||
SearchTypes type, String searchTerm, PagingData page) async {
|
||||
_logger.finest(() => 'Searching $type for term: $searchTerm');
|
||||
_networkStatusService.startSearchLoading();
|
||||
final url =
|
||||
'https://$serverName/api/v1/search?${page.toQueryParameters()}&${type.toQueryParameters()}&q=$searchTerm';
|
||||
final result = await _getApiPagedRequest(
|
||||
Uri.parse(url),
|
||||
);
|
||||
|
||||
_networkStatusService.finishSearchLoaing();
|
||||
return result
|
||||
.andThenSuccess((response) => response
|
||||
.map((json) => SearchResultMastodonExtensions.fromJson(json)))
|
||||
.execErrorCast();
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineClient extends FriendicaClient {
|
||||
static final _logger = Logger('$TimelineClient');
|
||||
|
||||
|
@ -876,6 +902,14 @@ abstract class FriendicaClient {
|
|||
.mapError((error) => error as ExecError);
|
||||
}
|
||||
|
||||
FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(
|
||||
Uri url) async {
|
||||
return (await getUrl(url, headers: _headers).andThenSuccessAsync(
|
||||
(response) async => response.map((data) => jsonDecode(data)),
|
||||
))
|
||||
.mapError((error) => error as ExecError);
|
||||
}
|
||||
|
||||
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
|
||||
return (await getUrl(url, headers: _headers).andThenSuccessAsync(
|
||||
(response) async => jsonDecode(response.data),
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import 'connection.dart';
|
||||
import 'timeline_entry.dart';
|
||||
|
||||
class SearchResults {
|
||||
final List<Connection> accounts;
|
||||
|
||||
final List<TimelineEntry> statuses;
|
||||
|
||||
final List<String> hashtags;
|
||||
|
||||
const SearchResults({
|
||||
required this.accounts,
|
||||
required this.statuses,
|
||||
required this.hashtags,
|
||||
});
|
||||
|
||||
factory SearchResults.empty() => const SearchResults(
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
);
|
||||
|
||||
SearchResults merge(SearchResults newResults) => SearchResults(
|
||||
accounts: [...accounts, ...newResults.accounts],
|
||||
statuses: [...statuses, ...newResults.statuses],
|
||||
hashtags: [...hashtags, ...newResults.hashtags],
|
||||
);
|
||||
|
||||
bool get isEmpty => accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchResults{#accounts: ${accounts.length}, #statuses: ${statuses.length}, #hashtags: ${hashtags.length}}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
enum SearchTypes {
|
||||
account,
|
||||
statusesText,
|
||||
directLink,
|
||||
hashTag,
|
||||
;
|
||||
|
||||
String toLabel() {
|
||||
switch (this) {
|
||||
case SearchTypes.hashTag:
|
||||
return 'Hashtag';
|
||||
case SearchTypes.account:
|
||||
return 'Account';
|
||||
case SearchTypes.statusesText:
|
||||
return 'Statuses Text';
|
||||
case SearchTypes.directLink:
|
||||
return 'Direct Link';
|
||||
}
|
||||
}
|
||||
|
||||
String toQueryParameters() {
|
||||
switch (this) {
|
||||
case SearchTypes.hashTag:
|
||||
return 'type=hashtags';
|
||||
case SearchTypes.account:
|
||||
return 'type=accounts';
|
||||
case SearchTypes.statusesText:
|
||||
return 'type=statuses';
|
||||
case SearchTypes.directLink:
|
||||
return 'resolve=true';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import 'screens/message_threads_browser_screen.dart';
|
|||
import 'screens/messages_new_thread.dart';
|
||||
import 'screens/notifications_screen.dart';
|
||||
import 'screens/post_screen.dart';
|
||||
import 'screens/search_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
import 'screens/sign_in.dart';
|
||||
import 'screens/splash.dart';
|
||||
|
@ -38,6 +39,7 @@ class ScreenPaths {
|
|||
static String userPosts = '/user_posts';
|
||||
static String likes = '/likes';
|
||||
static String reshares = '/reshares';
|
||||
static String search = '/search';
|
||||
}
|
||||
|
||||
bool needAuthChangeInitialized = true;
|
||||
|
@ -236,4 +238,12 @@ final appRouter = GoRouter(
|
|||
builder: (context, state) =>
|
||||
UserProfileScreen(userId: state.params['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: ScreenPaths.search,
|
||||
name: ScreenPaths.search,
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
name: ScreenPaths.search,
|
||||
child: SearchScreen(),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,334 @@
|
|||
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/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 {
|
||||
@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 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');
|
||||
setState(() {
|
||||
searching = true;
|
||||
});
|
||||
if (reset) {
|
||||
nextPage = PagingData(limit: limit);
|
||||
}
|
||||
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 (searching) {
|
||||
body = Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Searching for ${searchType.toLabel()} on: $searchText',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
body = buildResultBody(profile);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
drawer: 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;
|
||||
},
|
||||
onEditingComplete: () {
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
searchText = value;
|
||||
updateSearchResults(profile);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: searchType == SearchTypes.directLink
|
||||
? 'URL'
|
||||
: '${searchType.toLabel()} Search Text',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
updateSearchResults(profile);
|
||||
},
|
||||
child: const Text('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: AppBottomNavBar(
|
||||
currentButton: NavBarButtons.contacts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildResultBody(Profile profile) {
|
||||
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 => cm.fullRefresh(connection));
|
||||
if (context.mounted) {
|
||||
context.pushNamed(ScreenPaths.userProfile,
|
||||
params: {'id': connection.id});
|
||||
}
|
||||
},
|
||||
leading: ImageControl(
|
||||
imageUrl: connection.avatarUrl.toString(),
|
||||
iconOverride: const Icon(Icons.person),
|
||||
width: 32.0,
|
||||
onTap: () => context
|
||||
.pushNamed(ScreenPaths.userProfile, params: {'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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import '../../models/search_results.dart';
|
||||
import 'connection_mastodon_extensions.dart';
|
||||
import 'timeline_entry_mastodon_extensions.dart';
|
||||
|
||||
extension SearchResultMastodonExtensions on SearchResults {
|
||||
static SearchResults fromJson(Map<String, dynamic> json) {
|
||||
final accounts = (json['accounts'] as List<dynamic>? ?? [])
|
||||
.map((j) => ConnectionMastodonExtensions.fromJson(j))
|
||||
.toList();
|
||||
|
||||
final statuses = (json['statuses'] as List<dynamic>? ?? [])
|
||||
.map((j) => TimelineEntryMastodonExtensions.fromJson(j))
|
||||
.toList();
|
||||
|
||||
final hashtags = (json['hashtags'] as List<dynamic>? ?? [])
|
||||
.map((j) => j.toString())
|
||||
.toList();
|
||||
|
||||
return SearchResults(
|
||||
accounts: accounts,
|
||||
statuses: statuses,
|
||||
hashtags: hashtags,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ class NetworkStatusService {
|
|||
final interactionsLoadingStatus = ValueNotifier<bool>(false);
|
||||
final timelineLoadingStatus = ValueNotifier<bool>(false);
|
||||
final imageGalleryLoadingStatus = ValueNotifier<bool>(false);
|
||||
final searchLoadingStatus = ValueNotifier<bool>(false);
|
||||
|
||||
void startConnectionUpdateStatus() {
|
||||
connectionUpdateStatus.value = true;
|
||||
|
@ -55,4 +56,12 @@ class NetworkStatusService {
|
|||
void finishInteractionsLoading() {
|
||||
interactionsLoadingStatus.value = false;
|
||||
}
|
||||
|
||||
void startSearchLoading() {
|
||||
searchLoadingStatus.value = true;
|
||||
}
|
||||
|
||||
void finishSearchLoaing() {
|
||||
searchLoadingStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue