From 6bb29dfbc07fd6c7b88decd9838a9f832adbe97c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 29 May 2024 22:42:11 +0800 Subject: [PATCH] :sparkles: Realm posts --- devtools_options.yaml | 3 + lib/providers/content/post.dart | 9 +- lib/providers/content/realm.dart | 15 ++ lib/router.dart | 52 +++++-- lib/screens/channel/channel_chat.dart | 7 +- lib/screens/contact.dart | 4 +- lib/screens/posts/post_publish.dart | 31 ++-- lib/screens/realms.dart | 6 +- lib/screens/realms/realm_detail.dart | 18 +++ lib/screens/realms/realm_view.dart | 201 ++++++++++++++++++++++++++ lib/translations.dart | 10 +- lib/widgets/posts/post_list.dart | 2 +- 12 files changed, 321 insertions(+), 37 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/screens/realms/realm_detail.dart create mode 100644 lib/screens/realms/realm_view.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/providers/content/post.dart b/lib/providers/content/post.dart index 509b47c..5ae3b7d 100644 --- a/lib/providers/content/post.dart +++ b/lib/providers/content/post.dart @@ -7,8 +7,13 @@ class PostProvider extends GetConnect { httpClient.baseUrl = ServiceFinder.services['interactive']; } - Future listPost(int page) async { - final resp = await get('/api/feed?take=${10}&offset=$page'); + Future listPost(int page, {int? realm}) async { + final queries = [ + 'take=${10}', + 'offset=$page', + if (realm != null) 'realmId=$realm', + ]; + final resp = await get('/api/feed?${queries.join('&')}'); if (resp.statusCode != 200) { throw Exception(resp.body); } diff --git a/lib/providers/content/realm.dart b/lib/providers/content/realm.dart index 20ddc10..f0a30e4 100644 --- a/lib/providers/content/realm.dart +++ b/lib/providers/content/realm.dart @@ -3,6 +3,21 @@ import 'package:solian/providers/auth.dart'; import 'package:solian/services.dart'; class RealmProvider extends GetxController { + Future getRealm(String alias) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + final client = GetConnect(maxAuthRetries: 3); + client.httpClient.baseUrl = ServiceFinder.services['passport']; + + final resp = await client.get('/api/realms/$alias'); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } + Future listAvailableRealm() async { final AuthProvider auth = Get.find(); if (!await auth.isAuthorized) throw Exception('unauthorized'); diff --git a/lib/router.dart b/lib/router.dart index 9b5679b..f161b6f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,5 +1,6 @@ import 'package:go_router/go_router.dart'; import 'package:solian/models/channel.dart'; +import 'package:solian/models/realm.dart'; import 'package:solian/screens/account.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; @@ -9,7 +10,9 @@ import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/contact.dart'; import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/realms.dart'; +import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_organize.dart'; +import 'package:solian/screens/realms/realm_view.dart'; import 'package:solian/screens/social.dart'; import 'package:solian/screens/posts/post_publish.dart'; import 'package:solian/shells/basic_shell.dart'; @@ -57,6 +60,19 @@ abstract class AppRouter { ), ], ), + GoRoute( + path: '/posts/publish', + name: 'postPublishing', + builder: (context, state) { + final arguments = state.extra as PostPublishingArguments?; + return PostPublishingScreen( + edit: arguments?.edit, + reply: arguments?.reply, + repost: arguments?.repost, + realm: arguments?.realm, + ); + }, + ), ShellRoute( builder: (context, state, child) => BasicShell(state: state, child: child), @@ -87,19 +103,6 @@ abstract class AppRouter { ), ], ), - GoRoute( - path: '/posts/publish', - name: 'postPublishing', - builder: (context, state) { - final arguments = state.extra as PostPublishingArguments?; - return PostPublishingScreen( - edit: arguments?.edit, - reply: arguments?.reply, - repost: arguments?.repost, - realm: state.uri.queryParameters['realm'], - ); - }, - ), GoRoute( path: '/chat/organize', name: 'channelOrganizing', @@ -121,6 +124,20 @@ abstract class AppRouter { ); }, ), + ShellRoute( + builder: (context, state, child) => + BasicShell(state: state, child: child), + routes: [ + GoRoute( + path: '/realms/:alias/detail', + name: 'realmDetail', + builder: (context, state) => RealmDetailScreen( + realm: state.extra as Realm, + alias: state.pathParameters['alias']!, + ), + ), + ], + ), GoRoute( path: '/realm/organize', name: 'realmOrganizing', @@ -131,6 +148,15 @@ abstract class AppRouter { ); }, ), + GoRoute( + path: '/realm/:alias', + name: 'realmView', + builder: (context, state) { + return RealmViewScreen( + alias: state.pathParameters['alias']!, + ); + }, + ), ], ); } diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 146158a..0be1b76 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -161,12 +161,7 @@ class _ChannelChatScreenState extends State { ), ), onLongPress: () {}, - ).animate(key: Key('m${item.id}'), autoPlay: true).slideY( - curve: Curves.fastEaseInToSlowEaseOut, - duration: 350.ms, - begin: 0.25, - end: 0, - ); + ); } @override diff --git a/lib/screens/contact.dart b/lib/screens/contact.dart index 7aa4d14..985b837 100644 --- a/lib/screens/contact.dart +++ b/lib/screens/contact.dart @@ -165,10 +165,10 @@ class _ContactScreenState extends State { feColor: Colors.white, ), contentPadding: const EdgeInsets.symmetric(horizontal: 24), - title: Text(otherside.account.name), + title: Text(otherside.account.nick), subtitle: Text( 'channelDirectDescription' - .trParams({'username': otherside.account.name}), + .trParams({'username': '@${otherside.account.name}'}), ), onTap: () { AppRouter.instance.pushNamed( diff --git a/lib/screens/posts/post_publish.dart b/lib/screens/posts/post_publish.dart index 99b1e84..8b87621 100644 --- a/lib/screens/posts/post_publish.dart +++ b/lib/screens/posts/post_publish.dart @@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/post.dart'; +import 'package:solian/models/realm.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/router.dart'; import 'package:solian/services.dart'; @@ -16,15 +17,16 @@ class PostPublishingArguments { final Post? edit; final Post? reply; final Post? repost; + final Realm? realm; - PostPublishingArguments({this.edit, this.reply, this.repost}); + PostPublishingArguments({this.edit, this.reply, this.repost, this.realm}); } class PostPublishingScreen extends StatefulWidget { final Post? edit; final Post? reply; final Post? repost; - final String? realm; + final Realm? realm; const PostPublishingScreen({ super.key, @@ -73,6 +75,7 @@ class _PostPublishingScreenState extends State { if (widget.edit != null) 'alias': widget.edit!.alias, if (widget.reply != null) 'reply_to': widget.reply!.id, if (widget.repost != null) 'repost_to': widget.repost!.id, + if (widget.realm != null) 'realm': widget.realm!.alias, }; Response resp; @@ -226,7 +229,18 @@ class _PostPublishingScreenState extends State { } }, ), - const Divider(thickness: 0.3), + if (widget.realm != null) + MaterialBanner( + leading: const Icon(Icons.group), + leadingPadding: const EdgeInsets.only(left: 10, right: 20), + dividerColor: Colors.transparent, + content: Text( + 'postInRealmNotify' + .trParams({'realm': '#${widget.realm!.alias}'}), + ), + actions: notifyBannerActions, + ), + const Divider(thickness: 0.3, height: 0.3).paddingOnly(bottom: 8), Expanded( child: Container( padding: @@ -245,14 +259,9 @@ class _PostPublishingScreenState extends State { ), ), ), - Container( - constraints: const BoxConstraints(minHeight: 56), - decoration: BoxDecoration( - border: Border( - top: BorderSide( - width: 0.3, color: Theme.of(context).dividerColor), - ), - ), + const Divider(thickness: 0.3, height: 0.3), + SizedBox( + height: 56, child: Row( children: [ TextButton( diff --git a/lib/screens/realms.dart b/lib/screens/realms.dart index 45d2f2c..2067c01 100644 --- a/lib/screens/realms.dart +++ b/lib/screens/realms.dart @@ -154,7 +154,11 @@ class _RealmListScreenState extends State { ) ], ), - onTap: () {}, + onTap: () { + AppRouter.instance.pushNamed('realmView', pathParameters: { + 'alias': element.alias, + }); + }, ), ), ).paddingOnly(left: 8, right: 8, bottom: 4); diff --git a/lib/screens/realms/realm_detail.dart b/lib/screens/realms/realm_detail.dart new file mode 100644 index 0000000..50f7bf1 --- /dev/null +++ b/lib/screens/realms/realm_detail.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:solian/models/realm.dart'; + +class RealmDetailScreen extends StatelessWidget { + final String alias; + final Realm realm; + + const RealmDetailScreen({ + super.key, + required this.alias, + required this.realm, + }); + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/lib/screens/realms/realm_view.dart b/lib/screens/realms/realm_view.dart new file mode 100644 index 0000000..f84e513 --- /dev/null +++ b/lib/screens/realms/realm_view.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/models/realm.dart'; +import 'package:solian/providers/content/post.dart'; +import 'package:solian/providers/content/realm.dart'; +import 'package:solian/router.dart'; +import 'package:solian/screens/posts/post_publish.dart'; +import 'package:solian/theme.dart'; +import 'package:solian/widgets/posts/post_list.dart'; + +class RealmViewScreen extends StatefulWidget { + final String alias; + + const RealmViewScreen({super.key, required this.alias}); + + @override + State createState() => _RealmViewScreenState(); +} + +class _RealmViewScreenState extends State { + bool _isBusy = false; + String? _overrideAlias; + + Realm? _realm; + + getRealm({String? overrideAlias}) async { + final RealmProvider provider = Get.find(); + + setState(() => _isBusy = true); + + if (overrideAlias != null) { + _overrideAlias = overrideAlias; + } + + try { + final resp = await provider.getRealm(_overrideAlias ?? widget.alias); + setState(() => _realm = Realm.fromJson(resp.body)); + } catch (e) { + context.showErrorDialog(e); + } + + setState(() => _isBusy = false); + } + + @override + void initState() { + super.initState(); + + getRealm(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + child: DefaultTabController( + length: 2, + child: SafeArea( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: Text(_realm?.name ?? 'loading'.tr), + centerTitle: false, + actions: [ + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + AppRouter.instance + .pushNamed( + 'realmDetail', + pathParameters: {'alias': widget.alias}, + extra: _realm, + ) + .then((value) { + if (value == false) AppRouter.instance.pop(); + if (value != null) { + final resp = + Realm.fromJson(value as Map); + getRealm(overrideAlias: resp.alias); + } + }); + }, + ), + SizedBox( + width: SolianTheme.isLargeScreen(context) ? 8 : 16, + ), + ], + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab(icon: Icon(Icons.feed)), + Tab(icon: Icon(Icons.chat)), + ], + ), + ), + ) + ]; + }, + body: Builder( + builder: (context) { + if (_isBusy) { + return const Center(child: CircularProgressIndicator()); + } + + return TabBarView( + children: [ + RealmPostListWidget(realm: _realm!), + Icon(Icons.directions_transit), + ], + ); + }, + ), + ), + ), + ), + ); + } +} + +class RealmPostListWidget extends StatefulWidget { + final Realm realm; + + const RealmPostListWidget({super.key, required this.realm}); + + @override + State createState() => _RealmPostListWidgetState(); +} + +class _RealmPostListWidgetState extends State { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + getPosts(int pageKey) async { + final PostProvider provider = Get.find(); + + Response resp; + try { + resp = await provider.listPost(pageKey, realm: widget.realm.id); + } catch (e) { + _pagingController.error = e; + return; + } + + final PaginationResult result = PaginationResult.fromJson(resp.body); + final parsed = result.data?.map((e) => Post.fromJson(e)).toList(); + if (parsed != null && parsed.length >= 10) { + _pagingController.appendPage(parsed, pageKey + parsed.length); + } else if (parsed != null) { + _pagingController.appendLastPage(parsed); + } + } + + @override + void initState() { + super.initState(); + + _pagingController.addPageRequestListener(getPosts); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync(() => _pagingController.refresh()), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: ListTile( + leading: const Icon(Icons.post_add), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + tileColor: Theme.of(context).colorScheme.surfaceContainer, + title: Text('postNew'.tr), + subtitle: Text( + 'postNewInRealmHint' + .trParams({'realm': '#${widget.realm.alias}'}), + ), + onTap: () { + AppRouter.instance + .pushNamed( + 'postPublishing', + extra: PostPublishingArguments(realm: widget.realm), + ) + .then((value) { + if (value != null) _pagingController.refresh(); + }); + }, + ), + ), + PostListWidget(controller: _pagingController), + ], + ), + ); + } +} diff --git a/lib/translations.dart b/lib/translations.dart index 65bdf6e..835f592 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -68,10 +68,12 @@ class SolianMessages extends Translations { 'notifyAllRead': 'Mark all as read', 'notifyEmpty': 'All notifications read', 'notifyEmptyCaption': 'It seems like nothing happened recently', + 'postNew': 'Create a new post', + 'postNewInRealmHint': 'Add post in realm @realm', 'postAction': 'Post', 'postDetail': 'Post', 'postReplies': 'Replies', - 'postPublishing': 'Post a post', + 'postPublishing': 'Post post', 'postIdentityNotify': 'You will post this post as', 'postContentPlaceholder': 'What\'s happened?!', 'postReaction': 'Reactions of the Post', @@ -79,6 +81,7 @@ class SolianMessages extends Translations { 'postReplyAction': 'Make a reply', 'postRepliedNotify': 'Replied a post from @username.', 'postRepostedNotify': 'Reposted a post from @username.', + 'postInRealmNotify': 'You\'re posting in realm @realm.', 'postEditingNotify': 'You\'re editing as post from you.', 'postReplyingNotify': 'You\'re replying a post from @username.', 'postRepostingNotify': 'You\'re reposting a post from @username.', @@ -104,6 +107,7 @@ class SolianMessages extends Translations { 'realmDescription': 'Description', 'realmPublic': 'Public Realm', 'realmCommunity': 'Community Realm', + 'realmDetail': 'Realm detail', 'channelOrganizing': 'Organize a channel', 'channelOrganizeCommon': 'Create regular channel', 'channelOrganizeDirect': 'Create DM', @@ -190,6 +194,8 @@ class SolianMessages extends Translations { 'notifyAllRead': '已读所有通知', 'notifyEmpty': '通知箱为空', 'notifyEmptyCaption': '看起来最近没发生什么呢', + 'postNew': '创建新帖子', + 'postNewInRealmHint': '在领域 @realm 里发表新帖子', 'postAction': '发表', 'postDetail': '帖子详情', 'postReplies': '帖子回复', @@ -201,6 +207,7 @@ class SolianMessages extends Translations { 'postReplyAction': '发表一则回复', 'postRepliedNotify': '回了一个 @username 的帖子', 'postRepostedNotify': '转了一个 @username 的帖子', + 'postInRealmNotify': '你正在领域 @realm 中发表帖子', 'postEditingNotify': '你正在编辑一个你发布的帖子', 'postReplyingNotify': '你正在回一个来自 @username 的帖子', 'postRepostingNotify': '你正在转一个来自 @username 的帖子', @@ -225,6 +232,7 @@ class SolianMessages extends Translations { 'realmDescription': '领域简介', 'realmPublic': '公开领域', 'realmCommunity': '社区领域', + 'realmDetail': '领域详情', 'channelOrganizing': '组织频道', 'channelOrganizeCommon': '创建普通频道', 'channelOrganizeDirect': '创建私信频道', diff --git a/lib/widgets/posts/post_list.dart b/lib/widgets/posts/post_list.dart index b9734a4..2292783 100644 --- a/lib/widgets/posts/post_list.dart +++ b/lib/widgets/posts/post_list.dart @@ -48,7 +48,7 @@ class PostListWidget extends StatelessWidget { context: context, builder: (context) => PostAction(item: item), ).then((value) { - if (value == true) controller.refresh(); + if (value != null) controller.refresh(); }); }, );