From 4489b604d623c7a5a08d9219f61b93017a95b802 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 14 Apr 2024 01:07:57 +0800 Subject: [PATCH] :sparkles: Post details --- lib/i18n/app_en.arb | 4 +- lib/i18n/app_zh.arb | 4 +- lib/router.dart | 9 ++ lib/screens/explore.dart | 25 ++++- lib/screens/posts/screen.dart | 93 +++++++++++++++++++ lib/widgets/posts/comment_list.dart | 138 ++++++++++++++++++++++++++++ lib/widgets/posts/item.dart | 7 +- 7 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 lib/screens/posts/screen.dart create mode 100644 lib/widgets/posts/comment_list.dart diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index 7be074e..eff1fd3 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -5,5 +5,7 @@ "signIn": "Sign In", "signInCaption": "Sign in to create post, start a realm, message your friend and more!", "signUp": "Sign Up", - "signUpCaption": "Create an account on Solarpass and then get the access of entire Solar Networks!" + "signUpCaption": "Create an account on Solarpass and then get the access of entire Solar Networks!", + "post": "Post", + "comment": "Comment" } \ No newline at end of file diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb index 3fefa95..7fe6c15 100644 --- a/lib/i18n/app_zh.arb +++ b/lib/i18n/app_zh.arb @@ -5,5 +5,7 @@ "signIn": "登陆", "signInCaption": "登陆以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!", "signUp": "注册", - "signUpCaption": "在 Solarpass 注册一个账号以获得整个 Solar Networks 的存取权!" + "signUpCaption": "在 Solarpass 注册一个账号以获得整个 Solar Networks 的存取权!", + "post": "帖子", + "comment": "评论" } \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index f0193f1..28c3131 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,6 +1,7 @@ import 'package:go_router/go_router.dart'; import 'package:solian/screens/account.dart'; import 'package:solian/screens/explore.dart'; +import 'package:solian/screens/posts/screen.dart'; final router = GoRouter( routes: [ @@ -14,5 +15,13 @@ final router = GoRouter( name: 'account', builder: (context, state) => const AccountScreen(), ), + GoRoute( + path: '/posts/:dataset/:alias', + name: 'posts.screen', + builder: (context, state) => PostScreen( + alias: state.pathParameters['alias'] as String, + dataset: state.pathParameters['dataset'] as String, + ), + ), ], ); diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 8ea46d4..16e0d8d 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/models/post.dart'; +import 'package:solian/router.dart'; import 'package:solian/utils/service_url.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -65,12 +66,24 @@ class _ExploreScreenState extends State { ), child: Center( child: Container( - constraints: const BoxConstraints(maxWidth: 720), + constraints: const BoxConstraints(maxWidth: 640), child: PagedListView.separated( pagingController: _pagingController, - separatorBuilder: (context, index) => const Divider(thickness: 0.3), + separatorBuilder: (context, index) => + const Divider(thickness: 0.3), builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => PostItem(item: item), + itemBuilder: (context, item, index) => GestureDetector( + child: PostItem(item: item), + onTap: () { + router.goNamed( + 'posts.screen', + pathParameters: { + 'alias': item.alias, + 'dataset': '${item.modelType}s', + }, + ); + }, + ), ), ), ), @@ -78,4 +91,10 @@ class _ExploreScreenState extends State { ), ); } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } } diff --git a/lib/screens/posts/screen.dart b/lib/screens/posts/screen.dart new file mode 100644 index 0000000..e874911 --- /dev/null +++ b/lib/screens/posts/screen.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/posts/comment_list.dart'; +import 'package:solian/widgets/posts/item.dart'; +import 'package:solian/widgets/wrapper.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class PostScreen extends StatefulWidget { + final String dataset; + final String alias; + + const PostScreen({super.key, required this.alias, required this.dataset}); + + @override + State createState() => _PostScreenState(); +} + +class _PostScreenState extends State { + final _client = http.Client(); + + final PagingController _commentPagingController = + PagingController(firstPageKey: 0); + + Future fetchPost(BuildContext context) async { + final uri = getRequestUri( + 'interactive', '/api/p/${widget.dataset}/${widget.alias}'); + final res = await _client.get(uri); + if (res.statusCode != 200) { + final err = utf8.decode(res.bodyBytes); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Something went wrong... $err")), + ); + return null; + } else { + return Post.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + } + } + + @override + Widget build(BuildContext context) { + return LayoutWrapper( + title: AppLocalizations.of(context)!.post, + child: FutureBuilder( + future: fetchPost(context), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data != null) { + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 640), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: PostItem( + item: snapshot.data!, + brief: false, + ), + ), + SliverToBoxAdapter( + child: CommentListHeader( + related: snapshot.data!, + paging: _commentPagingController, + ), + ), + CommentList( + related: snapshot.data!, + dataset: widget.dataset, + paging: _commentPagingController, + ), + ], + ), + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } + + @override + void dispose() { + _commentPagingController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/posts/comment_list.dart b/lib/widgets/posts/comment_list.dart new file mode 100644 index 0000000..6ef0cef --- /dev/null +++ b/lib/widgets/posts/comment_list.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/models/post.dart'; +import 'package:http/http.dart' as http; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/posts/item.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CommentList extends StatefulWidget { + final Post related; + final String dataset; + final PagingController paging; + + const CommentList( + {super.key, + required this.related, + required this.dataset, + required this.paging}); + + @override + State createState() => _CommentListState(); +} + +class _CommentListState extends State { + final _client = http.Client(); + + Future fetchComments(int pageKey) async { + final offset = pageKey; + const take = 5; + + final alias = widget.related.alias; + + final uri = getRequestUri('interactive', + '/api/p/${widget.dataset}/$alias/comments?take=$take&offset=$offset'); + + final res = await _client.get(uri); + if (res.statusCode == 200) { + final result = + PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + final items = + result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty(); + final isLastPage = (result.count - pageKey) < take; + if (isLastPage || result.data == null) { + widget.paging.appendLastPage(items); + } else { + final nextPageKey = pageKey + items.length; + widget.paging.appendPage(items, nextPageKey); + } + } else { + widget.paging.error = utf8.decode(res.bodyBytes); + } + } + + @override + void initState() { + super.initState(); + widget.paging.addPageRequestListener((pageKey) { + fetchComments(pageKey); + }); + } + + @override + Widget build(BuildContext context) { + return PagedSliverList.separated( + pagingController: widget.paging, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => PostItem(item: item), + ), + separatorBuilder: (context, _) => const Divider(thickness: 0.3), + ); + } +} + +class CommentListHeader extends StatelessWidget { + final Post related; + final PagingController paging; + + const CommentListHeader( + {super.key, required this.related, required this.paging}); + + @override + Widget build(BuildContext context) { + final auth = context.read(); + + return Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 12.0, + ), + child: RichText( + text: TextSpan( + text: '${AppLocalizations.of(context)!.comment} ', + style: Theme.of(context).textTheme.headlineSmall, + children: [ + TextSpan( + text: '(${related.commentCount})', + style: const TextStyle( + fontFamily: 'monospaced', + fontSize: 16, + ), + ) + ]), + ), + ), + FutureBuilder( + future: auth.isAuthorized(), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data == true) { + return TextButton.icon( + icon: const Icon(Icons.add_comment_outlined), + label: const Text("LEAVE COMMENT"), + onPressed: () async { + final did = + await router.push("posts.comments.new", extra: related); + if (did == true) paging.refresh(); + }, + ); + } else { + return Container(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/posts/item.dart b/lib/widgets/posts/item.dart index 2a74186..2f1b553 100644 --- a/lib/widgets/posts/item.dart +++ b/lib/widgets/posts/item.dart @@ -7,8 +7,9 @@ import 'package:timeago/timeago.dart' as timeago; class PostItem extends StatefulWidget { final Post item; + final bool? brief; - const PostItem({super.key, required this.item}); + const PostItem({super.key, required this.item, this.brief}); @override State createState() => _PostItemState(); @@ -20,9 +21,9 @@ class _PostItemState extends State { Widget renderContent() { switch (widget.item.modelType) { case "article": - return ArticleContent(item: widget.item, brief: true); + return ArticleContent(item: widget.item, brief: widget.brief ?? true); default: - return MomentContent(item: widget.item, brief: true); + return MomentContent(item: widget.item, brief: widget.brief ?? true); } }