♻️ Refactored explore page
This commit is contained in:
parent
edd86eda77
commit
ea9ef9e82a
@ -584,6 +584,7 @@
|
|||||||
"colorSchemeBlack": "Black",
|
"colorSchemeBlack": "Black",
|
||||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||||
"postFeaturedComment": "Featured Comment",
|
"postFeaturedComment": "Featured Comment",
|
||||||
|
"postCategory": "Category",
|
||||||
"postCategoryTechnology": "Technology",
|
"postCategoryTechnology": "Technology",
|
||||||
"postCategoryGaming": "Gaming",
|
"postCategoryGaming": "Gaming",
|
||||||
"postCategoryLife": "Life",
|
"postCategoryLife": "Life",
|
||||||
@ -675,5 +676,12 @@
|
|||||||
"postThumbnail": "Post Thumbnail",
|
"postThumbnail": "Post Thumbnail",
|
||||||
"accountRealms": "Realms",
|
"accountRealms": "Realms",
|
||||||
"postInGlobal": "Global",
|
"postInGlobal": "Global",
|
||||||
"postInGlobalDescription": "Do not link this post with any realm."
|
"postInGlobalDescription": "Do not link this post with any realm.",
|
||||||
|
"postChannelGlobal": "Global",
|
||||||
|
"postChannelFriends": "Friends",
|
||||||
|
"postChannelFollowing": "Following",
|
||||||
|
"postChannelRealm": "Realms",
|
||||||
|
"postFilterReset": "Reset Filter",
|
||||||
|
"postFilterResetDescription": "Clear filter and show all posts.",
|
||||||
|
"postFilterWithCategory": "Viewing posts in {}"
|
||||||
}
|
}
|
||||||
|
@ -582,6 +582,7 @@
|
|||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||||
"postFeaturedComment": "精选评论",
|
"postFeaturedComment": "精选评论",
|
||||||
|
"postCategory": "分类",
|
||||||
"postCategoryTechnology": "技术",
|
"postCategoryTechnology": "技术",
|
||||||
"postCategoryGaming": "游戏",
|
"postCategoryGaming": "游戏",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -673,5 +674,12 @@
|
|||||||
"postThumbnail": "帖子缩略图",
|
"postThumbnail": "帖子缩略图",
|
||||||
"accountRealms": "领域",
|
"accountRealms": "领域",
|
||||||
"postInGlobal": "全站",
|
"postInGlobal": "全站",
|
||||||
"postInGlobalDescription": "不关联此帖子与任何领域。"
|
"postInGlobalDescription": "不关联此帖子与任何领域。",
|
||||||
|
"postChannelGlobal": "全站",
|
||||||
|
"postChannelFriends": "好友",
|
||||||
|
"postChannelFollowing": "关注",
|
||||||
|
"postChannelRealm": "领域",
|
||||||
|
"postFilterReset": "重置过滤器",
|
||||||
|
"postFilterResetDescription": "清除过滤器并显示所有帖子。",
|
||||||
|
"postFilterWithCategory": "查看{}区中的帖子"
|
||||||
}
|
}
|
||||||
|
@ -582,6 +582,7 @@
|
|||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||||
"postFeaturedComment": "精選評論",
|
"postFeaturedComment": "精選評論",
|
||||||
|
"postCategory": "分類",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -625,6 +626,7 @@
|
|||||||
"realmJoin": "加入領域",
|
"realmJoin": "加入領域",
|
||||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||||
|
"realmCommunityPublishersHint": "該領域的發佈者",
|
||||||
"realmJoined": "已加入領域 {}。",
|
"realmJoined": "已加入領域 {}。",
|
||||||
"join": "加入",
|
"join": "加入",
|
||||||
"pollEditorNew": "新投票",
|
"pollEditorNew": "新投票",
|
||||||
@ -669,5 +671,15 @@
|
|||||||
"attachmentBillingUploaded": "已佔用的字節數",
|
"attachmentBillingUploaded": "已佔用的字節數",
|
||||||
"attachmentBillingDiscount": "免費的字節數",
|
"attachmentBillingDiscount": "免費的字節數",
|
||||||
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
|
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
|
||||||
"postThumbnail": "帖子縮略圖"
|
"postThumbnail": "帖子縮略圖",
|
||||||
|
"accountRealms": "領域",
|
||||||
|
"postInGlobal": "全站",
|
||||||
|
"postInGlobalDescription": "不關聯此帖子與任何領域。",
|
||||||
|
"postChannelGlobal": "全站",
|
||||||
|
"postChannelFriends": "好友",
|
||||||
|
"postChannelFollowing": "關注",
|
||||||
|
"postChannelRealm": "領域",
|
||||||
|
"postFilterReset": "重置過濾器",
|
||||||
|
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
|
||||||
|
"postFilterWithCategory": "查看{}區中的帖子"
|
||||||
}
|
}
|
||||||
|
@ -582,6 +582,7 @@
|
|||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||||
"postFeaturedComment": "精選評論",
|
"postFeaturedComment": "精選評論",
|
||||||
|
"postCategory": "分類",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -625,6 +626,7 @@
|
|||||||
"realmJoin": "加入領域",
|
"realmJoin": "加入領域",
|
||||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||||
|
"realmCommunityPublishersHint": "該領域的發佈者",
|
||||||
"realmJoined": "已加入領域 {}。",
|
"realmJoined": "已加入領域 {}。",
|
||||||
"join": "加入",
|
"join": "加入",
|
||||||
"pollEditorNew": "新投票",
|
"pollEditorNew": "新投票",
|
||||||
@ -669,5 +671,15 @@
|
|||||||
"attachmentBillingUploaded": "已佔用的字節數",
|
"attachmentBillingUploaded": "已佔用的字節數",
|
||||||
"attachmentBillingDiscount": "免費的字節數",
|
"attachmentBillingDiscount": "免費的字節數",
|
||||||
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
|
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
|
||||||
"postThumbnail": "帖子縮略圖"
|
"postThumbnail": "帖子縮略圖",
|
||||||
|
"accountRealms": "領域",
|
||||||
|
"postInGlobal": "全站",
|
||||||
|
"postInGlobalDescription": "不關聯此帖子與任何領域。",
|
||||||
|
"postChannelGlobal": "全站",
|
||||||
|
"postChannelFriends": "好友",
|
||||||
|
"postChannelFollowing": "關注",
|
||||||
|
"postChannelRealm": "領域",
|
||||||
|
"postFilterReset": "重置過濾器",
|
||||||
|
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
|
||||||
|
"postFilterWithCategory": "查看{}區中的帖子"
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,7 @@ class SnPostContentProvider {
|
|||||||
Iterable<String>? categories,
|
Iterable<String>? categories,
|
||||||
Iterable<String>? tags,
|
Iterable<String>? tags,
|
||||||
String? realm,
|
String? realm,
|
||||||
|
String? channel,
|
||||||
}) 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,
|
||||||
@ -136,6 +137,7 @@ class SnPostContentProvider {
|
|||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||||
if (realm != null) 'realm': realm,
|
if (realm != null) 'realm': realm,
|
||||||
|
if (channel != null) 'channel': channel,
|
||||||
});
|
});
|
||||||
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)) ?? []),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||||
@ -8,7 +9,10 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.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/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.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/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@ -35,61 +39,47 @@ class ExploreScreen extends StatefulWidget {
|
|||||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExploreScreenState extends State<ExploreScreen> {
|
SnPostCategory? _selectedCategory;
|
||||||
|
|
||||||
|
class _ExploreScreenState extends State<ExploreScreen> with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _tabController = TabController(length: 4, vsync: this);
|
||||||
|
|
||||||
final _fabKey = GlobalKey<ExpandableFabState>();
|
final _fabKey = GlobalKey<ExpandableFabState>();
|
||||||
|
final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>());
|
||||||
|
|
||||||
bool _isBusy = true;
|
|
||||||
|
|
||||||
final List<SnPost> _posts = List.empty(growable: true);
|
|
||||||
final List<SnPostCategory> _categories = List.empty(growable: true);
|
final List<SnPostCategory> _categories = List.empty(growable: true);
|
||||||
int? _postCount;
|
|
||||||
|
|
||||||
String? _selectedCategory;
|
|
||||||
|
|
||||||
Future<void> _fetchCategories() async {
|
Future<void> _fetchCategories() async {
|
||||||
_categories.clear();
|
_categories.clear();
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/categories?take=100');
|
final resp = await sn.client.get('/cgi/co/categories?take=100');
|
||||||
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
|
setState(() {
|
||||||
|
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (mounted) context.showErrorDialog(err);
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
void _clearFilter() {
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
_selectedCategory = null;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
final pt = context.read<SnPostContentProvider>();
|
|
||||||
final result = await pt.listPosts(
|
|
||||||
take: 10,
|
|
||||||
offset: _posts.length,
|
|
||||||
categories: _selectedCategory != null ? [_selectedCategory!] : null,
|
|
||||||
);
|
|
||||||
final out = result.$1;
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
_postCount = result.$2;
|
|
||||||
_posts.addAll(out);
|
|
||||||
|
|
||||||
if (mounted) setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshPosts() {
|
|
||||||
_postCount = null;
|
|
||||||
_posts.clear();
|
|
||||||
return _fetchPosts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
|
||||||
_fetchPosts();
|
|
||||||
_fetchCategories();
|
_fetchCategories();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshPosts() async {
|
||||||
|
await _listKeys[_tabController.index].currentState?.refreshPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -131,7 +121,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -152,7 +142,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'articles',
|
'mode': 'articles',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -173,7 +163,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'questions',
|
'mode': 'questions',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -194,7 +184,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'videos',
|
'mode': 'videos',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -205,74 +195,116 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: NestedScrollView(
|
||||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
onRefresh: () => _refreshPosts(),
|
return [
|
||||||
child: CustomScrollView(
|
SliverOverlapAbsorber(
|
||||||
slivers: [
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenExplore').tr(),
|
title: Text('screenExplore').tr(),
|
||||||
floating: true,
|
floating: true,
|
||||||
snap: true,
|
snap: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.search),
|
icon: const Icon(Symbols.category),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed('postSearch');
|
showModalBottomSheet(
|
||||||
},
|
context: context,
|
||||||
),
|
builder: (context) => _PostCategoryPickerPopup(
|
||||||
const Gap(8),
|
categories: _categories,
|
||||||
],
|
selected: _selectedCategory,
|
||||||
bottom: PreferredSize(
|
),
|
||||||
preferredSize: const Size.fromHeight(50),
|
).then((value) {
|
||||||
child: SizedBox(
|
if (value != null && context.mounted) {
|
||||||
height: 50,
|
_selectedCategory = value == false ? null : value;
|
||||||
child: SingleChildScrollView(
|
refreshPosts();
|
||||||
scrollDirection: Axis.horizontal,
|
}
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
|
});
|
||||||
child: Row(
|
},
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: _categories.map((ele) {
|
|
||||||
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);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.search),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('postSearch');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.globe, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postChannelGlobal').tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.group, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postChannelFriends').tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.subscriptions, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postChannelFollowing').tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.workspaces, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postChannelRealm').tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverGap(12),
|
];
|
||||||
SliverInfiniteList(
|
},
|
||||||
itemCount: _posts.length,
|
body: TabBarView(
|
||||||
isLoading: _isBusy,
|
controller: _tabController,
|
||||||
centerLoading: true,
|
children: [
|
||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
_PostListWidget(
|
||||||
onFetchData: _fetchPosts,
|
key: _listKeys[0],
|
||||||
itemBuilder: (context, idx) {
|
onClearFilter: _clearFilter,
|
||||||
return OpenablePostItem(
|
),
|
||||||
data: _posts[idx],
|
_PostListWidget(
|
||||||
maxWidth: 640,
|
key: _listKeys[1],
|
||||||
onChanged: (data) {
|
channel: 'friends',
|
||||||
setState(() => _posts[idx] = data);
|
onClearFilter: _clearFilter,
|
||||||
},
|
),
|
||||||
onDeleted: () {
|
_PostListWidget(
|
||||||
_refreshPosts();
|
key: _listKeys[2],
|
||||||
},
|
channel: 'following',
|
||||||
);
|
onClearFilter: _clearFilter,
|
||||||
},
|
),
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
_PostListWidget(
|
||||||
|
key: _listKeys[3],
|
||||||
|
withRealm: true,
|
||||||
|
onClearFilter: _clearFilter,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -280,3 +312,233 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostListWidget extends StatefulWidget {
|
||||||
|
final String? channel;
|
||||||
|
final bool withRealm;
|
||||||
|
final Function onClearFilter;
|
||||||
|
|
||||||
|
const _PostListWidget({super.key, this.channel, this.withRealm = false, required this.onClearFilter});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PostListWidget> createState() => _PostListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostListWidgetState extends State<_PostListWidget> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
|
final List<SnRealm> _realms = List.empty(growable: true);
|
||||||
|
SnRealm? _selectedRealm;
|
||||||
|
int? _postCount;
|
||||||
|
|
||||||
|
Future<void> _fetchRealms() async {
|
||||||
|
try {
|
||||||
|
final rels = context.read<SnRealmProvider>();
|
||||||
|
final out = await rels.listAvailableRealms();
|
||||||
|
setState(() {
|
||||||
|
_realms.addAll(out);
|
||||||
|
_selectedRealm = out.firstOrNull;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchPosts() async {
|
||||||
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final pt = context.read<SnPostContentProvider>();
|
||||||
|
final result = await pt.listPosts(
|
||||||
|
take: 10,
|
||||||
|
offset: _posts.length,
|
||||||
|
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
|
||||||
|
channel: widget.channel,
|
||||||
|
realm: _selectedRealm?.alias,
|
||||||
|
);
|
||||||
|
final out = result.$1;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
_postCount = result.$2;
|
||||||
|
_posts.addAll(out);
|
||||||
|
|
||||||
|
if (mounted) setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshPosts() {
|
||||||
|
_postCount = null;
|
||||||
|
_posts.clear();
|
||||||
|
return _fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.withRealm) {
|
||||||
|
_fetchRealms().then((_) {
|
||||||
|
_fetchPosts();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_fetchPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (_selectedCategory != null)
|
||||||
|
MaterialBanner(
|
||||||
|
content: Text(
|
||||||
|
'postFilterWithCategory'.tr(args: [
|
||||||
|
'postCategory${_selectedCategory!.alias.capitalize()}'.trExists()
|
||||||
|
? 'postCategory${_selectedCategory!.alias.capitalize()}'.tr()
|
||||||
|
: _selectedCategory!.name,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
leading: Icon(kCategoryIcons[_selectedCategory!.alias] ?? Symbols.question_mark),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.clear),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onClearFilter.call();
|
||||||
|
refreshPosts();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 4),
|
||||||
|
),
|
||||||
|
if (widget.withRealm)
|
||||||
|
DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<SnRealm>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: _realms
|
||||||
|
.map(
|
||||||
|
(ele) => DropdownMenuItem<SnRealm>(
|
||||||
|
value: ele,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AccountImage(
|
||||||
|
content: ele.avatar,
|
||||||
|
fallbackWidget: const Icon(Symbols.group, size: 16),
|
||||||
|
radius: 14,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
ele.name,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
value: _selectedRealm,
|
||||||
|
onChanged: (SnRealm? value) {
|
||||||
|
setState(() => _selectedRealm = value);
|
||||||
|
refreshPosts();
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.only(left: 4, right: 12),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.withRealm) const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||||
|
onRefresh: () => refreshPosts(),
|
||||||
|
child: InfiniteList(
|
||||||
|
itemCount: _posts.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
centerLoading: true,
|
||||||
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
|
onFetchData: _fetchPosts,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
return OpenablePostItem(
|
||||||
|
data: _posts[idx],
|
||||||
|
maxWidth: 640,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _posts[idx] = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
refreshPosts();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(top: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostCategoryPickerPopup extends StatelessWidget {
|
||||||
|
final List<SnPostCategory> categories;
|
||||||
|
final SnPostCategory? selected;
|
||||||
|
|
||||||
|
const _PostCategoryPickerPopup({required this.categories, this.selected});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.category, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postCategory').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.clear),
|
||||||
|
title: Text('postFilterReset').tr(),
|
||||||
|
subtitle: Text('postFilterResetDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: categories
|
||||||
|
.map(
|
||||||
|
(ele) => ChoiceChip(
|
||||||
|
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
|
||||||
|
label: Text(
|
||||||
|
'postCategory${ele.alias.capitalize()}'.trExists()
|
||||||
|
? 'postCategory${ele.alias.capitalize()}'.tr()
|
||||||
|
: ele.name,
|
||||||
|
),
|
||||||
|
selected: ele == selected,
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value) {
|
||||||
|
Navigator.pop(context, ele);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
).padding(horizontal: 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user