From daee3e8074208fd1d08345054ddfb0b3763cf4d6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 25 May 2024 13:19:16 +0800 Subject: [PATCH] :sparkles: Post detail --- lib/providers/auth.dart | 3 ++ lib/providers/content/post_explore.dart | 20 +++++++- lib/router.dart | 14 ++++++ lib/screens/posts/post_detail.dart | 59 ++++++++++++++++++++++++ lib/screens/social.dart | 21 ++++++--- lib/translations.dart | 2 + lib/widgets/posts/post_item.dart | 32 ++++++++++++- lib/widgets/posts/post_quick_action.dart | 10 ++-- 8 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 lib/screens/posts/post_detail.dart diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index ec93e36..36ce850 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -97,6 +97,7 @@ class AuthProvider extends GetConnect { ); Get.find().connect(); + Get.find().notifyPrefetch(); return credentials!; } @@ -105,6 +106,8 @@ class AuthProvider extends GetConnect { _cacheUserProfileResponse = null; Get.find().disconnect(); + Get.find().notifications.clear(); + Get.find().notificationUnread.value = 0; storage.deleteAll(); } diff --git a/lib/providers/content/post_explore.dart b/lib/providers/content/post_explore.dart index dfff724..0c6fdbc 100644 --- a/lib/providers/content/post_explore.dart +++ b/lib/providers/content/post_explore.dart @@ -1,11 +1,27 @@ import 'package:get/get.dart'; import 'package:solian/services.dart'; -class PostExploreProvider extends GetConnect { +class PostProvider extends GetConnect { @override void onInit() { httpClient.baseUrl = ServiceFinder.services['interactive']; } - Future listPost(int page) => get('/api/feed?take=${10}&offset=$page'); + Future listPost(int page) async { + final resp = await get('/api/feed?take=${10}&offset=$page'); + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + return resp; + } + + Future getPost(String alias) async { + final resp = await get('/api/posts/$alias'); + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + return resp; + } } diff --git a/lib/router.dart b/lib/router.dart index ac6bb44..ab19ac7 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -3,6 +3,7 @@ import 'package:solian/screens/account.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/contact.dart'; +import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/social.dart'; import 'package:solian/screens/posts/publish.dart'; import 'package:solian/shells/basic_shell.dart'; @@ -32,6 +33,19 @@ abstract class AppRouter { ), ], ), + ShellRoute( + builder: (context, state, child) => + BasicShell(state: state, child: child), + routes: [ + GoRoute( + path: '/posts/:alias', + name: 'postDetail', + builder: (context, state) => PostDetailScreen( + alias: state.pathParameters['alias']!, + ), + ), + ], + ), ShellRoute( builder: (context, state, child) => BasicShell(state: state, child: child), diff --git a/lib/screens/posts/post_detail.dart b/lib/screens/posts/post_detail.dart new file mode 100644 index 0000000..bc408c0 --- /dev/null +++ b/lib/screens/posts/post_detail.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/providers/content/post_explore.dart'; +import 'package:solian/widgets/posts/post_item.dart'; + +class PostDetailScreen extends StatefulWidget { + final String alias; + + const PostDetailScreen({super.key, required this.alias}); + + @override + State createState() => _PostDetailScreenState(); +} + +class _PostDetailScreenState extends State { + Post? item; + + Future getDetail() async { + final PostProvider provider = Get.find(); + + try { + final resp = await provider.getPost(widget.alias); + item = Post.fromJson(resp.body); + } catch (e) { + context.showErrorDialog(e).then((_) => Navigator.pop(context)); + } + + return item; + } + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + child: FutureBuilder( + future: getDetail(), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return Column( + children: [ + PostItem( + item: item!, + isClickable: true, + isShowReply: false, + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/screens/social.dart b/lib/screens/social.dart index f1b4597..164e6a3 100644 --- a/lib/screens/social.dart +++ b/lib/screens/social.dart @@ -23,10 +23,13 @@ class _SocialScreenState extends State { PagingController(firstPageKey: 0); getPosts(int pageKey) async { - final PostExploreProvider provider = Get.find(); - final resp = await provider.listPost(pageKey); - if (resp.statusCode != 200) { - _pagingController.error = resp.bodyString; + final PostProvider provider = Get.find(); + + Response resp; + try { + resp = await provider.listPost(pageKey); + } catch (e) { + _pagingController.error = e; return; } @@ -41,7 +44,7 @@ class _SocialScreenState extends State { @override void initState() { - Get.lazyPut(() => PostExploreProvider()); + Get.lazyPut(() => PostProvider()); super.initState(); _pagingController.addPageRequestListener(getPosts); @@ -104,11 +107,17 @@ class _SocialScreenState extends State { child: PostItem( key: Key('p${item.alias}'), item: item, + isClickable: true, ).paddingSymmetric( vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0, ), - onTap: () {}, + onTap: () { + AppRouter.instance.pushNamed( + 'postDetail', + pathParameters: {'alias': item.alias}, + ); + }, onLongPress: () { showModalBottomSheet( useRootNavigator: true, diff --git a/lib/translations.dart b/lib/translations.dart index 3287c8d..38d02ff 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -62,6 +62,7 @@ class SolianMessages extends Translations { 'notifyEmpty': 'All notifications read', 'notifyEmptyCaption': 'It seems like nothing happened recently', 'postAction': 'Post', + 'postDetail': 'Post', 'postPublishing': 'Post a post', 'postIdentityNotify': 'You will post this post as', 'postContentPlaceholder': 'What\'s happened?!', @@ -140,6 +141,7 @@ class SolianMessages extends Translations { 'notifyEmpty': '通知箱为空', 'notifyEmptyCaption': '看起来最近没发生什么呢', 'postAction': '发表', + 'postDetail': '帖子详情', 'postPublishing': '发表帖子', 'postIdentityNotify': '你将会以本身份发表帖子', 'postContentPlaceholder': '发生什么事了?!', diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 64a58f0..4a0c2a1 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:solian/models/post.dart'; +import 'package:solian/router.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/posts/post_quick_action.dart'; @@ -11,14 +12,18 @@ import 'package:timeago/timeago.dart' show format; class PostItem extends StatefulWidget { final Post item; + final bool isClickable; final bool isCompact; final bool isReactable; + final bool isShowReply; const PostItem({ super.key, required this.item, + this.isClickable = false, this.isCompact = false, this.isReactable = true, + this.isShowReply = true, }); @override @@ -158,9 +163,31 @@ class _PostItemState extends State { padding: const EdgeInsets.all(0), ).paddingOnly(left: 12, right: 8), if (widget.item.replyTo != null) - buildReply(context).paddingOnly(top: 4), + GestureDetector( + child: buildReply(context).paddingOnly(top: 4), + onTap: () { + if (!widget.isClickable) return; + AppRouter.instance.pushNamed( + 'postDetail', + pathParameters: { + 'alias': widget.item.replyTo!.alias, + }, + ); + }, + ), if (widget.item.repostTo != null) - buildRepost(context).paddingOnly(top: 4), + GestureDetector( + child: buildRepost(context).paddingOnly(top: 4), + onTap: () { + if (!widget.isClickable) return; + AppRouter.instance.pushNamed( + 'postDetail', + pathParameters: { + 'alias': widget.item.repostTo!.alias, + }, + ); + }, + ), ], ), ) @@ -173,6 +200,7 @@ class _PostItemState extends State { ), AttachmentList(attachmentsId: item.attachments ?? List.empty()), PostQuickAction( + isShowReply: widget.isShowReply, isReactable: widget.isReactable, item: widget.item, onReact: (symbol, changes) { diff --git a/lib/widgets/posts/post_quick_action.dart b/lib/widgets/posts/post_quick_action.dart index ab4b196..073ea3b 100644 --- a/lib/widgets/posts/post_quick_action.dart +++ b/lib/widgets/posts/post_quick_action.dart @@ -10,11 +10,13 @@ import 'package:solian/widgets/posts/post_reaction.dart'; class PostQuickAction extends StatefulWidget { final Post item; final bool isReactable; + final bool isShowReply; final void Function(String symbol, int num) onReact; const PostQuickAction({ super.key, required this.item, + this.isShowReply = true, this.isReactable = true, required this.onReact, }); @@ -93,17 +95,17 @@ class _PostQuickActionState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (widget.isReactable) + if (widget.isReactable && widget.isShowReply) ActionChip( avatar: const Icon(Icons.comment), label: Text(widget.item.replyCount.toString()), visualDensity: density, onPressed: () {}, ), - if (widget.isReactable) + if (widget.isReactable && widget.isShowReply) const VerticalDivider( thickness: 0.3, width: 0.3, indent: 8, endIndent: 8) - .paddingOnly(left: 8), + .paddingSymmetric(horizontal: 8), Expanded( child: ListView( shrinkWrap: true, @@ -132,7 +134,7 @@ class _PostQuickActionState extends State { onPressed: () => showReactMenu(), ), ], - ).paddingOnly(left: 8), + ), ) ], ),