From 1f4aa8916dd154f81b7b8fe2340df9fd8d0b5cc3 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 13 Oct 2024 21:48:53 +0800 Subject: [PATCH] :sparkles: Search posts --- lib/providers/content/posts.dart | 21 +++ lib/router.dart | 6 +- lib/screens/explore.dart | 7 + lib/screens/feed/search.dart | 99 ------------ lib/screens/{feed => posts}/draft_box.dart | 0 lib/screens/posts/post_search.dart | 157 +++++++++++++++++++ lib/widgets/attachments/attachment_list.dart | 12 +- lib/widgets/loading_indicator.dart | 2 +- 8 files changed, 198 insertions(+), 106 deletions(-) delete mode 100644 lib/screens/feed/search.dart rename lib/screens/{feed => posts}/draft_box.dart (100%) create mode 100644 lib/screens/posts/post_search.dart diff --git a/lib/providers/content/posts.dart b/lib/providers/content/posts.dart index e78361f..bd7bdb1 100644 --- a/lib/providers/content/posts.dart +++ b/lib/providers/content/posts.dart @@ -54,6 +54,27 @@ class PostProvider extends GetxController { return resp; } + Future searchPost(String probe, int page, + {String? realm, String? author, tag, category, int take = 10}) async { + final queries = [ + 'probe=$probe', + 'take=$take', + 'offset=$page', + if (tag != null) 'tag=$tag', + if (category != null) 'category=$category', + if (author != null) 'author=$author', + if (realm != null) 'realm=$realm', + ]; + final AuthProvider auth = Get.find(); + final client = await auth.configureClient('co'); + final resp = await client.get('/posts/search?${queries.join('&')}'); + if (resp.statusCode != 200) { + throw RequestException(resp); + } + + return resp; + } + Future listPost(int page, {String? realm, String? author, tag, category, int take = 10}) async { final queries = [ diff --git a/lib/router.dart b/lib/router.dart index 09ccd16..8755ec4 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -18,9 +18,9 @@ import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/chat.dart'; import 'package:solian/screens/dashboard.dart'; -import 'package:solian/screens/feed/search.dart'; +import 'package:solian/screens/posts/post_search.dart'; import 'package:solian/screens/posts/post_detail.dart'; -import 'package:solian/screens/feed/draft_box.dart'; +import 'package:solian/screens/posts/draft_box.dart'; import 'package:solian/screens/realms.dart'; import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_organize.dart'; @@ -96,7 +96,7 @@ abstract class AppRouter { name: 'postSearch', builder: (context, state) => TitleShell( state: state, - child: FeedSearchScreen( + child: PostSearchScreen( tag: state.uri.queryParameters['tag'], category: state.uri.queryParameters['category'], ), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index a8204a7..217ab79 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -7,6 +7,7 @@ import 'package:get/get.dart'; import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/navigation.dart'; +import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart'; @@ -159,6 +160,12 @@ class _ExploreScreenState extends State toolbarHeight: AppTheme.toolbarHeight(context), leading: AppBarLeadingButton.adaptive(context), actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + AppRouter.instance.pushNamed('postSearch'); + }, + ), const BackgroundStateWidget(), const NotificationButton(), SizedBox( diff --git a/lib/screens/feed/search.dart b/lib/screens/feed/search.dart deleted file mode 100644 index d913cbd..0000000 --- a/lib/screens/feed/search.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:solian/models/pagination.dart'; -import 'package:solian/providers/content/posts.dart'; -import 'package:solian/widgets/posts/post_list.dart'; - -import '../../models/post.dart'; - -class FeedSearchScreen extends StatefulWidget { - final String? tag; - final String? category; - - const FeedSearchScreen({super.key, this.tag, this.category}); - - @override - State createState() => _FeedSearchScreenState(); -} - -class _FeedSearchScreenState extends State { - final PagingController _pagingController = - PagingController(firstPageKey: 0); - - getPosts(int pageKey) async { - final PostProvider provider = Get.find(); - - Response resp; - try { - resp = await provider.listPost( - pageKey, - tag: widget.tag, - category: widget.category, - ); - } catch (e) { - _pagingController.error = e; - return; - } - - final PaginationResult result = PaginationResult.fromJson(resp.body); - final parsed = result.data?.map((e) => Post.fromJson(e)).toList(); - if (parsed != null && parsed.length >= 10) { - _pagingController.appendPage(parsed, pageKey + parsed.length); - } else if (parsed != null) { - _pagingController.appendLastPage(parsed); - } - } - - @override - void initState() { - super.initState(); - - _pagingController.addPageRequestListener(getPosts); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Material( - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - if (widget.tag != null) - ListTile( - leading: const Icon(Icons.label), - tileColor: Theme.of(context).colorScheme.surfaceContainer, - title: Text('postSearchWithTag'.trParams({'key': widget.tag!})), - ), - if (widget.category != null) - ListTile( - leading: const Icon(Icons.category), - tileColor: Theme.of(context).colorScheme.surfaceContainer, - title: Text('postSearchWithCategory' - .trParams({'key': widget.category!})), - ), - Expanded( - child: RefreshIndicator( - onRefresh: () => Future.sync(() => _pagingController.refresh()), - child: CustomScrollView( - slivers: [ - ControlledPostListWidget( - controller: _pagingController, - onUpdate: () => _pagingController.refresh(), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } -} diff --git a/lib/screens/feed/draft_box.dart b/lib/screens/posts/draft_box.dart similarity index 100% rename from lib/screens/feed/draft_box.dart rename to lib/screens/posts/draft_box.dart diff --git a/lib/screens/posts/post_search.dart b/lib/screens/posts/post_search.dart new file mode 100644 index 0000000..a6ffa7a --- /dev/null +++ b/lib/screens/posts/post_search.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/providers/content/posts.dart'; +import 'package:solian/widgets/loading_indicator.dart'; +import 'package:solian/widgets/posts/post_list.dart'; + +import '../../models/post.dart'; + +class PostSearchScreen extends StatefulWidget { + final String? tag; + final String? category; + + const PostSearchScreen({super.key, this.tag, this.category}); + + @override + State createState() => _PostSearchScreenState(); +} + +class _PostSearchScreenState extends State { + final TextEditingController _probeController = TextEditingController(); + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + bool _isBusy = true; + + _searchPosts(int pageKey) async { + if (widget.tag == null && + widget.category == null && + _probeController.text.isEmpty) { + _pagingController.appendLastPage([]); + return; + } + + if (!_isBusy) { + setState(() => _isBusy = true); + } + + if (pageKey == 0) { + _pagingController.itemList?.clear(); + _pagingController.nextPageKey = 0; + } + + final PostProvider provider = Get.find(); + + Response resp; + try { + if (_probeController.text.isEmpty) { + resp = await provider.listPost( + pageKey, + tag: widget.tag, + category: widget.category, + ); + } else { + resp = await provider.searchPost( + _probeController.text, + pageKey, + tag: widget.tag, + category: widget.category, + ); + } + } catch (e) { + _pagingController.error = e; + return; + } + + final PaginationResult result = PaginationResult.fromJson(resp.body); + final parsed = result.data?.map((e) => Post.fromJson(e)).toList(); + if (parsed != null && parsed.length >= 10) { + _pagingController.appendPage(parsed, pageKey + parsed.length); + } else if (parsed != null) { + _pagingController.appendLastPage(parsed); + } + + setState(() => _isBusy = false); + } + + @override + void initState() { + super.initState(); + _pagingController.addPageRequestListener(_searchPosts); + } + + @override + void dispose() { + _probeController.dispose(); + _pagingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + if (widget.tag != null) + ListTile( + leading: const Icon(Icons.label), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + tileColor: Theme.of(context) + .colorScheme + .surfaceContainer + .withOpacity(0.5), + title: Text('postSearchWithTag'.trParams({'key': widget.tag!})), + ), + if (widget.category != null) + ListTile( + leading: const Icon(Icons.category), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + tileColor: Theme.of(context) + .colorScheme + .surfaceContainer + .withOpacity(0.5), + title: Text('postSearchWithCategory'.trParams({ + 'key': widget.category!, + })), + ), + Container( + color: Theme.of(context) + .colorScheme + .secondaryContainer + .withOpacity(0.5), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: TextField( + controller: _probeController, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: 'search'.tr, + ), + onSubmitted: (_) { + _searchPosts(0); + }, + ), + ), + if (_isBusy) const LoadingIndicator(), + Expanded( + child: RefreshIndicator( + onRefresh: () => Future.sync(() => _pagingController.refresh()), + child: CustomScrollView( + slivers: [ + ControlledPostListWidget( + controller: _pagingController, + onUpdate: () => _pagingController.refresh(), + ), + SliverGap(MediaQuery.of(context).padding.bottom), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index bebf8f2..b0e1da6 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -186,7 +186,9 @@ class _AttachmentListState extends State { if (widget.isFullWidth && _attachments.length == 1) { final element = _attachments.first; - double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; + final isImage = element!.mimetype.split('/').firstOrNull == 'image'; + double ratio = + element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9); return Container( width: MediaQuery.of(context).size.width, constraints: BoxConstraints( @@ -260,7 +262,9 @@ class _AttachmentListState extends State { final element = _attachments[idx]; idx++; if (element == null) return const SizedBox.shrink(); - double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9; + final isImage = element.mimetype.split('/').firstOrNull == 'image'; + double ratio = + element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9); return Container( constraints: BoxConstraints( maxWidth: widget.columnMaxWidth, @@ -303,7 +307,9 @@ class _AttachmentListState extends State { itemBuilder: (context, idx) { final element = _attachments[idx]; if (element == null) const SizedBox.shrink(); - final ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; + final isImage = element!.mimetype.split('/').firstOrNull == 'image'; + double ratio = + element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9); return Container( constraints: BoxConstraints( maxWidth: math.min( diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index 7f8f910..ea74fd9 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -9,7 +9,7 @@ class LoadingIndicator extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), - color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.8), + color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,