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 '../routes.dart';
|
||||||
import '../services/notifications_manager.dart';
|
import '../services/notifications_manager.dart';
|
||||||
import '../utils/active_profile_selector.dart';
|
import '../utils/active_profile_selector.dart';
|
||||||
import '../utils/snackbar_builder.dart';
|
|
||||||
|
|
||||||
enum NavBarButtons {
|
enum NavBarButtons {
|
||||||
timelines,
|
timelines,
|
||||||
|
@ -51,7 +50,7 @@ class AppBottomNavBar extends StatelessWidget {
|
||||||
context.pushNamed(ScreenPaths.contacts);
|
context.pushNamed(ScreenPaths.contacts);
|
||||||
break;
|
break;
|
||||||
case NavBarButtons.search:
|
case NavBarButtons.search:
|
||||||
buildSnackbar(context, 'Search screen coming soon...');
|
context.pushNamed(ScreenPaths.search);
|
||||||
break;
|
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 {
|
class StatusHeaderControl extends StatelessWidget {
|
||||||
static final _logger = Logger('$StatusHeaderControl');
|
static final _logger = Logger('$StatusHeaderControl');
|
||||||
final TimelineEntry entry;
|
final TimelineEntry entry;
|
||||||
|
final bool showIsCommentText;
|
||||||
|
|
||||||
const StatusHeaderControl({
|
const StatusHeaderControl({
|
||||||
super.key,
|
super.key,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
|
this.showIsCommentText = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
void goToProfile(BuildContext context, String id) {
|
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(
|
Row(
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'services/connections_manager.dart';
|
||||||
import 'services/direct_message_service.dart';
|
import 'services/direct_message_service.dart';
|
||||||
import 'services/entry_manager_service.dart';
|
import 'services/entry_manager_service.dart';
|
||||||
import 'services/feature_version_checker.dart';
|
import 'services/feature_version_checker.dart';
|
||||||
|
import 'services/fediverse_server_validator.dart';
|
||||||
import 'services/follow_requests_manager.dart';
|
import 'services/follow_requests_manager.dart';
|
||||||
import 'services/gallery_service.dart';
|
import 'services/gallery_service.dart';
|
||||||
import 'services/hashtag_service.dart';
|
import 'services/hashtag_service.dart';
|
||||||
|
@ -36,6 +37,8 @@ Future<void> dependencyInjectionInitialization() async {
|
||||||
getIt.registerSingleton<IHashtagRepo>(ObjectBoxHashtagRepo());
|
getIt.registerSingleton<IHashtagRepo>(ObjectBoxHashtagRepo());
|
||||||
getIt.registerSingleton<HashtagService>(HashtagService());
|
getIt.registerSingleton<HashtagService>(HashtagService());
|
||||||
getIt.registerSingleton<NetworkStatusService>(NetworkStatusService());
|
getIt.registerSingleton<NetworkStatusService>(NetworkStatusService());
|
||||||
|
getIt.registerSingleton<FediverseServiceValidator>(
|
||||||
|
FediverseServiceValidator());
|
||||||
getIt.registerSingleton<FriendicaVersionChecker>(
|
getIt.registerSingleton<FriendicaVersionChecker>(
|
||||||
const FriendicaVersionChecker());
|
const FriendicaVersionChecker());
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ import '../models/group_data.dart';
|
||||||
import '../models/image_entry.dart';
|
import '../models/image_entry.dart';
|
||||||
import '../models/instance_info.dart';
|
import '../models/instance_info.dart';
|
||||||
import '../models/media_attachment_uploads/image_types_enum.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/timeline_entry.dart';
|
||||||
import '../models/user_notification.dart';
|
import '../models/user_notification.dart';
|
||||||
import '../models/visibility.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/group_data_mastodon_extensions.dart';
|
||||||
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
||||||
import '../serializers/mastodon/notification_mastodon_extension.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/timeline_entry_mastodon_extensions.dart';
|
||||||
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
|
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
|
||||||
import '../services/network_status_service.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 {
|
class TimelineClient extends FriendicaClient {
|
||||||
static final _logger = Logger('$TimelineClient');
|
static final _logger = Logger('$TimelineClient');
|
||||||
|
|
||||||
|
@ -876,6 +902,14 @@ abstract class FriendicaClient {
|
||||||
.mapError((error) => error as ExecError);
|
.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 {
|
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
|
||||||
return (await getUrl(url, headers: _headers).andThenSuccessAsync(
|
return (await getUrl(url, headers: _headers).andThenSuccessAsync(
|
||||||
(response) async => jsonDecode(response.data),
|
(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/messages_new_thread.dart';
|
||||||
import 'screens/notifications_screen.dart';
|
import 'screens/notifications_screen.dart';
|
||||||
import 'screens/post_screen.dart';
|
import 'screens/post_screen.dart';
|
||||||
|
import 'screens/search_screen.dart';
|
||||||
import 'screens/settings_screen.dart';
|
import 'screens/settings_screen.dart';
|
||||||
import 'screens/sign_in.dart';
|
import 'screens/sign_in.dart';
|
||||||
import 'screens/splash.dart';
|
import 'screens/splash.dart';
|
||||||
|
@ -38,6 +39,7 @@ class ScreenPaths {
|
||||||
static String userPosts = '/user_posts';
|
static String userPosts = '/user_posts';
|
||||||
static String likes = '/likes';
|
static String likes = '/likes';
|
||||||
static String reshares = '/reshares';
|
static String reshares = '/reshares';
|
||||||
|
static String search = '/search';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needAuthChangeInitialized = true;
|
bool needAuthChangeInitialized = true;
|
||||||
|
@ -236,4 +238,12 @@ final appRouter = GoRouter(
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
UserProfileScreen(userId: state.params['id']!),
|
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 interactionsLoadingStatus = ValueNotifier<bool>(false);
|
||||||
final timelineLoadingStatus = ValueNotifier<bool>(false);
|
final timelineLoadingStatus = ValueNotifier<bool>(false);
|
||||||
final imageGalleryLoadingStatus = ValueNotifier<bool>(false);
|
final imageGalleryLoadingStatus = ValueNotifier<bool>(false);
|
||||||
|
final searchLoadingStatus = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
void startConnectionUpdateStatus() {
|
void startConnectionUpdateStatus() {
|
||||||
connectionUpdateStatus.value = true;
|
connectionUpdateStatus.value = true;
|
||||||
|
@ -55,4 +56,12 @@ class NetworkStatusService {
|
||||||
void finishInteractionsLoading() {
|
void finishInteractionsLoading() {
|
||||||
interactionsLoadingStatus.value = false;
|
interactionsLoadingStatus.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void startSearchLoading() {
|
||||||
|
searchLoadingStatus.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishSearchLoaing() {
|
||||||
|
searchLoadingStatus.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue