From 277ba695131d29fac23767b689c2d17b5208d52b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 26 Jul 2024 16:53:05 +0800 Subject: [PATCH] :sparkles: Account profile page --- lib/controllers/post_list_controller.dart | 24 ++- lib/providers/content/posts.dart | 3 +- lib/router.dart | 8 + lib/screens/account/profile_page.dart | 192 ++++++++++++++++++ lib/translations/en_us.dart | 3 + lib/translations/zh_cn.dart | 3 + lib/widgets/account/account_heading.dart | 33 +-- .../account/account_profile_popup.dart | 34 +++- lib/widgets/attachments/attachment_list.dart | 25 ++- lib/widgets/posts/post_item.dart | 14 +- 10 files changed, 291 insertions(+), 48 deletions(-) create mode 100644 lib/screens/account/profile_page.dart diff --git a/lib/controllers/post_list_controller.dart b/lib/controllers/post_list_controller.dart index eabbc50..64c6c13 100644 --- a/lib/controllers/post_list_controller.dart +++ b/lib/controllers/post_list_controller.dart @@ -5,6 +5,8 @@ import 'package:solian/models/post.dart'; import 'package:solian/providers/content/posts.dart'; class PostListController extends GetxController { + String? author; + /// The polling source modifier. /// - `0`: default recommendations /// - `1`: shuffle mode @@ -13,9 +15,9 @@ class PostListController extends GetxController { /// The paging controller for infinite loading. /// Only available when mode is `0`. PagingController pagingController = - PagingController(firstPageKey: 0); + PagingController(firstPageKey: 0); - PostListController() { + PostListController({this.author}) { _initPagingController(); } @@ -48,6 +50,7 @@ class PostListController extends GetxController { RxBool isPreparing = false.obs; RxInt focusCursor = 0.obs; + Post get focusPost => postList[focusCursor.value]; RxInt postTotal = 0.obs; @@ -102,17 +105,24 @@ class PostListController extends GetxController { Response resp; try { - resp = await provider.listRecommendations( - pageKey, - channel: mode.value == 0 ? null : 'shuffle', - ); + if (author != null) { + resp = await provider.listPost( + pageKey, + author: author, + ); + } else { + resp = await provider.listRecommendations( + pageKey, + channel: mode.value == 0 ? null : 'shuffle', + ); + } } catch (e) { rethrow; } finally { isBusy.value = false; } - final PaginationResult result = PaginationResult.fromJson(resp.body); + final result = PaginationResult.fromJson(resp.body); final out = result.data?.map((e) => Post.fromJson(e)).toList(); postTotal.value = result.count; diff --git a/lib/providers/content/posts.dart b/lib/providers/content/posts.dart index 924b227..24d6ac8 100644 --- a/lib/providers/content/posts.dart +++ b/lib/providers/content/posts.dart @@ -45,12 +45,13 @@ class PostProvider extends GetConnect { } Future listPost(int page, - {int? realm, String? tag, category}) async { + {int? realm, String? author, tag, category}) async { final queries = [ 'take=${10}', 'offset=$page', if (tag != null) 'tag=$tag', if (category != null) 'category=$category', + if (author != null) 'author=$author', if (realm != null) 'realmId=$realm', ]; final resp = await get('/posts?${queries.join('&')}'); diff --git a/lib/router.dart b/lib/router.dart index a87c79e..7569e7f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,6 +4,7 @@ import 'package:solian/screens/about.dart'; import 'package:solian/screens/account.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; +import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_organize.dart'; @@ -201,6 +202,13 @@ abstract class AppRouter { child: const PersonalizeScreen(), ), ), + GoRoute( + path: '/account/view/:name', + name: 'accountProfilePage', + builder: (context, state) => AccountProfilePage( + name: state.pathParameters['name']!, + ), + ), GoRoute( path: '/about', name: 'about', diff --git a/lib/screens/account/profile_page.dart b/lib/screens/account/profile_page.dart new file mode 100644 index 0000000..437d31e --- /dev/null +++ b/lib/screens/account/profile_page.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/controllers/post_list_controller.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/models/account.dart'; +import 'package:solian/models/attachment.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/screens/account/notification.dart'; +import 'package:solian/services.dart'; +import 'package:solian/theme.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; +import 'package:solian/widgets/app_bar_leading.dart'; +import 'package:solian/widgets/attachments/attachment_item.dart'; +import 'package:solian/widgets/current_state_action.dart'; +import 'package:solian/widgets/feed/feed_list.dart'; +import 'package:solian/widgets/sized_container.dart'; + +class AccountProfilePage extends StatefulWidget { + final String name; + + const AccountProfilePage({super.key, required this.name}); + + @override + State createState() => _AccountProfilePageState(); +} + +class _AccountProfilePageState extends State { + late final PostListController _postController; + final PagingController _albumPagingController = + PagingController(firstPageKey: 0); + + bool _isBusy = true; + + Account? _userinfo; + + Future getUserinfo() async { + setState(() => _isBusy = true); + + final client = ServiceFinder.configureClient('auth'); + final resp = await client.get('/users/${widget.name}'); + if (resp.statusCode == 200) { + _userinfo = Account.fromJson(resp.body); + setState(() => _isBusy = false); + } else { + context.showErrorDialog(resp.bodyString).then((_) { + Navigator.pop(context); + }); + } + } + + @override + void initState() { + super.initState(); + _postController = PostListController(author: widget.name); + _albumPagingController.addPageRequestListener((pageKey) async { + final client = ServiceFinder.configureClient('files'); + final resp = await client + .get('/attachments?take=10&offset=$pageKey&author=${widget.name}'); + if (resp.statusCode == 200) { + final result = PaginationResult.fromJson(resp.body); + final out = result.data?.map((e) => Attachment.fromJson(e)).toList(); + if (out != null && out.length >= 10) { + _albumPagingController.appendPage(out, pageKey + out.length); + } else if (out != null) { + _albumPagingController.appendLastPage(out); + } + } else { + _albumPagingController.error = resp.bodyString; + } + }); + getUserinfo(); + } + + @override + Widget build(BuildContext context) { + if (_isBusy || _userinfo == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Material( + color: Theme.of(context).colorScheme.surface, + child: DefaultTabController( + length: 2, + child: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + centerTitle: false, + floating: true, + toolbarHeight: SolianTheme.toolbarHeight(context), + leadingWidth: 24, + leading: AppBarLeadingButton.adaptive(context), + flexibleSpace: Row( + children: [ + const SizedBox(width: 16), + if (_userinfo != null) + AccountAvatar(content: _userinfo!.avatar, radius: 16), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_userinfo != null) + Text( + _userinfo!.nick, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (_userinfo != null) + Text( + '@${_userinfo!.name}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + const BackgroundStateWidget(), + const NotificationButton(), + SizedBox( + width: SolianTheme.isLargeScreen(context) ? 8 : 16, + ), + ], + ), + bottom: TabBar( + tabs: [ + Tab(text: 'profilePosts'.tr), + Tab(text: 'profileAlbum'.tr), + ], + ), + ) + ]; + }, + body: TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: [ + RefreshIndicator( + onRefresh: () => _postController.reloadAllOver(), + child: CustomScrollView(slivers: [ + if (_userinfo == null) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ), + if (_userinfo != null) + FeedListWidget( + controller: _postController.pagingController, + ), + ]), + ), + CenteredContainer( + child: RefreshIndicator( + onRefresh: () => + Future.sync(() => _albumPagingController.refresh()), + child: PagedGridView( + pagingController: _albumPagingController, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (BuildContext context, item, int index) { + const radius = BorderRadius.all(Radius.circular(8)); + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 0.3, + ), + borderRadius: radius, + ), + child: ClipRRect( + borderRadius: radius, + child: AttachmentItem( + item: item, + parentId: 'album', + ), + ), + ); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index 40a58e1..f0abf89 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -14,6 +14,9 @@ const messagesEnglish = { 'feedSearch': 'Search Feed', 'feedSearchWithTag': 'Searching with tag #@key', 'feedSearchWithCategory': 'Searching in category @category', + 'visitProfilePage': 'Visit Profile Page', + 'profilePosts': 'Posts', + 'profileAlbum': 'Album', 'chat': 'Chat', 'apply': 'Apply', 'cancel': 'Cancel', diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index a0154c0..cfbcc59 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -21,6 +21,9 @@ const simplifiedChineseMessages = { 'feedSearch': '搜索资讯', 'feedSearchWithTag': '检索带有 #@key 标签的资讯', 'feedSearchWithCategory': '检索位于分类 @category 的资讯', + 'visitProfilePage': '造访个人主页', + 'profilePosts': '帖子', + 'profileAlbum': '相簿', 'chat': '聊天', 'apply': '应用', 'search': '搜索', diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index d2dac85..bcec3ca 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -18,6 +18,7 @@ class AccountHeadingWidget extends StatelessWidget { final String? desc; final Account? detail; final List? badges; + final List? extraWidgets; final Future? status; final Function? onEditStatus; @@ -32,6 +33,7 @@ class AccountHeadingWidget extends StatelessWidget { required this.badges, this.detail, this.status, + this.extraWidgets, this.onEditStatus, }); @@ -149,19 +151,6 @@ class AccountHeadingWidget extends StatelessWidget { ], ).paddingOnly(left: 116, top: 6), const SizedBox(height: 4), - if (detail?.suspendedAt != null) - SizedBox( - width: double.infinity, - child: Card( - child: ListTile( - title: Text('accountSuspended'.tr), - subtitle: Text('accountSuspendedAt'.trParams({ - 'date': DateFormat('y/M/d').format(detail!.suspendedAt!), - })), - trailing: const Icon(Icons.block), - ), - ), - ).paddingOnly(left: 16, right: 16), if (badges?.isNotEmpty ?? false) SizedBox( width: double.infinity, @@ -177,7 +166,21 @@ class AccountHeadingWidget extends StatelessWidget { vertical: PlatformInfo.isMobile ? 0 : 6, ), ), - ).paddingOnly(left: 16, right: 16), + ).paddingSymmetric(horizontal: 16), + ...?extraWidgets?.map((x) => x.paddingSymmetric(horizontal: 16)), + if (detail?.suspendedAt != null) + SizedBox( + width: double.infinity, + child: Card( + child: ListTile( + title: Text('accountSuspended'.tr), + subtitle: Text('accountSuspendedAt'.trParams({ + 'date': DateFormat('y/M/d').format(detail!.suspendedAt!), + })), + trailing: const Icon(Icons.block), + ), + ), + ).paddingSymmetric(horizontal: 16), SizedBox( width: double.infinity, child: Card( @@ -188,7 +191,7 @@ class AccountHeadingWidget extends StatelessWidget { ), ), ), - ).paddingOnly(left: 16, right: 16), + ).paddingSymmetric(horizontal: 16), ], ), ); diff --git a/lib/widgets/account/account_profile_popup.dart b/lib/widgets/account/account_profile_popup.dart index 826447c..2fd5759 100644 --- a/lib/widgets/account/account_profile_popup.dart +++ b/lib/widgets/account/account_profile_popup.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/account.dart'; import 'package:solian/providers/account_status.dart'; +import 'package:solian/router.dart'; import 'package:solian/services.dart'; import 'package:solian/widgets/account/account_heading.dart'; @@ -23,9 +24,7 @@ class _AccountProfilePopupState extends State { void getUserinfo() async { setState(() => _isBusy = true); - final client = GetConnect(); - client.httpClient.baseUrl = ServiceFinder.buildUrl('auth', null); - + final client = ServiceFinder.configureClient('auth'); final resp = await client.get('/users/${widget.account.name}'); if (resp.statusCode == 200) { _userinfo = Account.fromJson(resp.body); @@ -39,14 +38,16 @@ class _AccountProfilePopupState extends State { @override void initState() { super.initState(); - getUserinfo(); } @override Widget build(BuildContext context) { if (_isBusy || _userinfo == null) { - return const Center(child: CircularProgressIndicator()); + return SizedBox( + height: MediaQuery.of(context).size.height * 0.75, + child: const Center(child: CircularProgressIndicator()), + ); } return SizedBox( @@ -62,7 +63,28 @@ class _AccountProfilePopupState extends State { desc: _userinfo!.description, detail: _userinfo!, badges: _userinfo!.badges, - status: Get.find().getSomeoneStatus(_userinfo!.name), + status: + Get.find().getSomeoneStatus(_userinfo!.name), + extraWidgets: [ + Card( + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + title: Text('visitProfilePage'.tr), + visualDensity: + const VisualDensity(horizontal: -4, vertical: -2), + trailing: const Icon(Icons.chevron_right), + onTap: () { + AppRouter.instance.goNamed( + 'accountProfilePage', + pathParameters: {'name': _userinfo!.name}, + ); + Navigator.pop(context); + }, + ), + ), + ], ).paddingOnly(top: 16), ], ), diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index f74d3b9..f5466ca 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -14,6 +14,7 @@ class AttachmentList extends StatefulWidget { final List attachmentsId; final bool isGrid; final bool isForceGrid; + final double flatMaxHeight; final double? width; final double? viewport; @@ -24,6 +25,7 @@ class AttachmentList extends StatefulWidget { required this.attachmentsId, this.isGrid = false, this.isForceGrid = false, + this.flatMaxHeight = 720, this.width, this.viewport, }); @@ -255,20 +257,25 @@ class _AttachmentListState extends State { itemBuilder: (context, idx) { final element = _attachmentsMeta[idx]; return Container( - decoration: BoxDecoration( - border: - Border.all(color: Theme.of(context).dividerColor, width: 1), - borderRadius: radius, - ), - child: ClipRRect( - borderRadius: radius, - child: _buildEntry(element, idx), - )); + decoration: BoxDecoration( + border: + Border.all(color: Theme.of(context).dividerColor, width: 1), + borderRadius: radius, + ), + child: ClipRRect( + borderRadius: radius, + child: _buildEntry(element, idx), + ), + ); }, ).paddingSymmetric(horizontal: 24); } return Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints( + maxHeight: widget.flatMaxHeight, + ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHigh, border: Border.symmetric( diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 41feaca..e1c8f06 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -298,16 +298,10 @@ class _PostItemState extends State { right: 16, left: 16, ), - Container( - width: MediaQuery.of(context).size.width, - constraints: const BoxConstraints( - maxHeight: 720, - ), - child: AttachmentList( - parentId: widget.item.id.toString(), - attachmentsId: attachments, - isGrid: attachments.length > 1, - ), + AttachmentList( + parentId: widget.item.id.toString(), + attachmentsId: attachments, + isGrid: attachments.length > 1, ), if (widget.isShowReply && widget.isReactable) PostQuickAction(