From 85d2eff7f8324599e3db9377d0d8a1803c0aee3f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 22 Dec 2024 17:19:35 +0800 Subject: [PATCH] :sparkles: Explore page filtered by post --- lib/providers/post.dart | 4 ++ lib/screens/explore.dart | 101 ++++++++++++++++++++++++-------- lib/widgets/post/post_item.dart | 2 +- 3 files changed, 83 insertions(+), 24 deletions(-) diff --git a/lib/providers/post.dart b/lib/providers/post.dart index c6330f1..0886fde 100644 --- a/lib/providers/post.dart +++ b/lib/providers/post.dart @@ -83,12 +83,16 @@ class SnPostContentProvider { int offset = 0, String? type, String? author, + Iterable? categories, + Iterable? tags, }) async { final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { 'take': take, 'offset': offset, if (type != null) 'type': type, if (author != null) 'author': author, + if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), + if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), }); final List out = await _preloadRelatedDataInBatch( List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 7323df1..71cbdbb 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -5,12 +5,27 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/post.dart'; +import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; +import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; +const Map kCategoryIcons = { + 'technology': Symbols.tools_wrench, + 'gaming': Symbols.gamepad, + 'life': Symbols.nightlife, + 'arts': Symbols.format_paint, + 'sports': Symbols.sports_soccer, + 'music': Symbols.music_note, + 'news': Symbols.newspaper, + 'knowledge': Symbols.book, + 'literature': Symbols.book, +}; + class ExploreScreen extends StatefulWidget { const ExploreScreen({super.key}); @@ -24,15 +39,34 @@ class _ExploreScreenState extends State { bool _isBusy = true; final List _posts = List.empty(growable: true); + final List _categories = List.empty(growable: true); int? _postCount; + String? _selectedCategory; + + Future _fetchCategories() async { + _categories.clear(); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/co/categories?take=100'); + _categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast() ?? []); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } + } + Future _fetchPosts() async { if (_postCount != null && _posts.length >= _postCount!) return; setState(() => _isBusy = true); final pt = context.read(); - final result = await pt.listPosts(take: 10, offset: _posts.length); + final result = await pt.listPosts( + take: 10, + offset: _posts.length, + categories: _selectedCategory != null ? [_selectedCategory!] : null, + ); final out = result.$1; if (!mounted) return; @@ -43,10 +77,17 @@ class _ExploreScreenState extends State { if (mounted) setState(() => _isBusy = false); } + Future _refreshPosts() { + _postCount = null; + _posts.clear(); + return _fetchPosts(); + } + @override void initState() { super.initState(); _fetchPosts(); + _fetchCategories(); } @override @@ -59,27 +100,20 @@ class _ExploreScreenState extends State { type: ExpandableFabType.up, childrenAnimation: ExpandableFabAnimation.none, overlayStyle: ExpandableFabOverlayStyle( - color: Theme.of(context) - .colorScheme - .surface - .withAlpha((255 * 0.5).round()), + color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()), ), openButtonBuilder: RotateFloatingActionButtonBuilder( child: const Icon(Symbols.add, size: 28), fabSize: ExpandableFabSize.regular, - foregroundColor: - Theme.of(context).floatingActionButtonTheme.foregroundColor, - backgroundColor: - Theme.of(context).floatingActionButtonTheme.backgroundColor, + foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, + backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, shape: const CircleBorder(), ), closeButtonBuilder: DefaultFloatingActionButtonBuilder( child: const Icon(Symbols.close, size: 28), fabSize: ExpandableFabSize.regular, - foregroundColor: - Theme.of(context).floatingActionButtonTheme.foregroundColor, - backgroundColor: - Theme.of(context).floatingActionButtonTheme.backgroundColor, + foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, + backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, shape: const CircleBorder(), ), children: [ @@ -95,8 +129,7 @@ class _ExploreScreenState extends State { 'mode': 'stories', }).then((value) { if (value == true) { - _posts.clear(); - _fetchPosts(); + _refreshPosts(); } }); _fabKey.currentState!.toggle(); @@ -117,8 +150,7 @@ class _ExploreScreenState extends State { 'mode': 'articles', }).then((value) { if (value == true) { - _posts.clear(); - _fetchPosts(); + _refreshPosts(); } }); _fabKey.currentState!.toggle(); @@ -131,10 +163,7 @@ class _ExploreScreenState extends State { ), body: RefreshIndicator( displacement: 40 + MediaQuery.of(context).padding.top, - onRefresh: () { - _posts.clear(); - return _fetchPosts(); - }, + onRefresh: () => _refreshPosts(), child: CustomScrollView( slivers: [ SliverAppBar( @@ -151,6 +180,33 @@ class _ExploreScreenState extends State { ), const Gap(8), ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(40), + child: SizedBox( + height: 40, + child: ListView.builder( + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + scrollDirection: Axis.horizontal, + itemCount: _categories.length, + itemBuilder: (context, idx) { + final ele = _categories[idx]; + return StyledWidget(ChoiceChip( + avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark), + label: Text( + 'postCategory${ele.alias.capitalize()}'.trExists() + ? 'postCategory${ele.alias.capitalize()}'.tr() + : ele.name, + ), + selected: _selectedCategory == ele.alias, + onSelected: (value) { + _selectedCategory = value ? ele.alias : null; + _refreshPosts(); + }, + )).padding(horizontal: 4); + }, + ), + ), + ), ), SliverInfiniteList( itemCount: _posts.length, @@ -167,8 +223,7 @@ class _ExploreScreenState extends State { setState(() => _posts[idx] = data); }, onDeleted: () { - _posts.clear(); - _fetchPosts(); + _refreshPosts(); }, ), onTap: () { diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 5c0e4aa..e5af559 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -245,7 +245,7 @@ class PostItem extends StatelessWidget { horizontal: 16, vertical: 4, ), - if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, bottom: 6), + if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6), ], ), ),