Explore page filtered by post

This commit is contained in:
LittleSheep 2024-12-22 17:19:35 +08:00
parent 2375c46852
commit 85d2eff7f8
3 changed files with 83 additions and 24 deletions

View File

@ -83,12 +83,16 @@ class SnPostContentProvider {
int offset = 0, int offset = 0,
String? type, String? type,
String? author, String? author,
Iterable<String>? categories,
Iterable<String>? tags,
}) async { }) async {
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
'take': take, 'take': take,
'offset': offset, 'offset': offset,
if (type != null) 'type': type, if (type != null) 'type': type,
if (author != null) 'author': author, if (author != null) 'author': author,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
}); });
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),

View File

@ -5,12 +5,27 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.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:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
const Map<String, IconData> 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 { class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key}); const ExploreScreen({super.key});
@ -24,15 +39,34 @@ class _ExploreScreenState extends State<ExploreScreen> {
bool _isBusy = true; bool _isBusy = true;
final List<SnPost> _posts = List.empty(growable: true); final List<SnPost> _posts = List.empty(growable: true);
final List<SnPostCategory> _categories = List.empty(growable: true);
int? _postCount; int? _postCount;
String? _selectedCategory;
Future<void> _fetchCategories() async {
_categories.clear();
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/categories?take=100');
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
Future<void> _fetchPosts() async { Future<void> _fetchPosts() async {
if (_postCount != null && _posts.length >= _postCount!) return; if (_postCount != null && _posts.length >= _postCount!) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
final pt = context.read<SnPostContentProvider>(); final pt = context.read<SnPostContentProvider>();
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; final out = result.$1;
if (!mounted) return; if (!mounted) return;
@ -43,10 +77,17 @@ class _ExploreScreenState extends State<ExploreScreen> {
if (mounted) setState(() => _isBusy = false); if (mounted) setState(() => _isBusy = false);
} }
Future<void> _refreshPosts() {
_postCount = null;
_posts.clear();
return _fetchPosts();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchPosts(); _fetchPosts();
_fetchCategories();
} }
@override @override
@ -59,27 +100,20 @@ class _ExploreScreenState extends State<ExploreScreen> {
type: ExpandableFabType.up, type: ExpandableFabType.up,
childrenAnimation: ExpandableFabAnimation.none, childrenAnimation: ExpandableFabAnimation.none,
overlayStyle: ExpandableFabOverlayStyle( overlayStyle: ExpandableFabOverlayStyle(
color: Theme.of(context) color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()),
.colorScheme
.surface
.withAlpha((255 * 0.5).round()),
), ),
openButtonBuilder: RotateFloatingActionButtonBuilder( openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Symbols.add, size: 28), child: const Icon(Symbols.add, size: 28),
fabSize: ExpandableFabSize.regular, fabSize: ExpandableFabSize.regular,
foregroundColor: foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
Theme.of(context).floatingActionButtonTheme.foregroundColor, backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(), shape: const CircleBorder(),
), ),
closeButtonBuilder: DefaultFloatingActionButtonBuilder( closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Symbols.close, size: 28), child: const Icon(Symbols.close, size: 28),
fabSize: ExpandableFabSize.regular, fabSize: ExpandableFabSize.regular,
foregroundColor: foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
Theme.of(context).floatingActionButtonTheme.foregroundColor, backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(), shape: const CircleBorder(),
), ),
children: [ children: [
@ -95,8 +129,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'stories', 'mode': 'stories',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_posts.clear(); _refreshPosts();
_fetchPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@ -117,8 +150,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'articles', 'mode': 'articles',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_posts.clear(); _refreshPosts();
_fetchPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@ -131,10 +163,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
body: RefreshIndicator( body: RefreshIndicator(
displacement: 40 + MediaQuery.of(context).padding.top, displacement: 40 + MediaQuery.of(context).padding.top,
onRefresh: () { onRefresh: () => _refreshPosts(),
_posts.clear();
return _fetchPosts();
},
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
@ -151,6 +180,33 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
const Gap(8), 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( SliverInfiniteList(
itemCount: _posts.length, itemCount: _posts.length,
@ -167,8 +223,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
setState(() => _posts[idx] = data); setState(() => _posts[idx] = data);
}, },
onDeleted: () { onDeleted: () {
_posts.clear(); _refreshPosts();
_fetchPosts();
}, },
), ),
onTap: () { onTap: () {

View File

@ -245,7 +245,7 @@ class PostItem extends StatelessWidget {
horizontal: 16, horizontal: 16,
vertical: 4, 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),
], ],
), ),
), ),