From 7b7988e6cb3c0c919c516e31b595f9f244202a94 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 12 Oct 2024 00:41:03 +0800 Subject: [PATCH] :recycle: Refactored post layout --- lib/screens/account/profile_edit.dart | 2 +- lib/screens/account/profile_page.dart | 3 +- lib/screens/chat.dart | 2 +- lib/screens/dashboard.dart | 2 +- lib/screens/explore.dart | 105 +---- lib/screens/posts/post_detail.dart | 10 +- lib/screens/realms.dart | 2 +- lib/widgets/account/account_avatar.dart | 55 ++- lib/widgets/account/account_heading.dart | 2 +- lib/widgets/account/account_select.dart | 2 +- lib/widgets/account/relative_list.dart | 2 +- lib/widgets/account/relative_select.dart | 2 +- .../attachments/attachment_fullscreen.dart | 2 +- lib/widgets/attachments/attachment_list.dart | 151 +++++--- lib/widgets/channel/channel_list.dart | 4 +- lib/widgets/channel/channel_member.dart | 3 +- lib/widgets/chat/call/call_no_content.dart | 2 +- lib/widgets/chat/chat_event.dart | 5 +- lib/widgets/chat/chat_message_input.dart | 2 +- .../navigation/app_account_widget.dart | 2 +- lib/widgets/navigation/realm_switcher.dart | 2 +- lib/widgets/posts/post_creation.dart | 108 ++++++ lib/widgets/posts/post_item.dart | 362 ++++++++---------- lib/widgets/posts/post_list.dart | 43 ++- lib/widgets/posts/post_quick_action.dart | 8 +- lib/widgets/posts/post_replies.dart | 25 +- lib/widgets/realms/realm_member.dart | 3 +- 27 files changed, 504 insertions(+), 407 deletions(-) create mode 100644 lib/widgets/posts/post_creation.dart diff --git a/lib/screens/account/profile_edit.dart b/lib/screens/account/profile_edit.dart index 62654a5..d4b4765 100644 --- a/lib/screens/account/profile_edit.dart +++ b/lib/screens/account/profile_edit.dart @@ -192,7 +192,7 @@ class _PersonalizeScreenState extends State { const Gap(24), Stack( children: [ - AccountAvatar(content: _avatar, radius: 40), + AttachedCircleAvatar(content: _avatar, radius: 40), Positioned( bottom: 0, left: 40, diff --git a/lib/screens/account/profile_page.dart b/lib/screens/account/profile_page.dart index 48eecf8..5e5581f 100644 --- a/lib/screens/account/profile_page.dart +++ b/lib/screens/account/profile_page.dart @@ -260,7 +260,8 @@ class _AccountProfilePageState extends State { const Gap(8), const Gap(8), if (_userinfo != null) - AccountAvatar(content: _userinfo!.avatar, radius: 16), + AttachedCircleAvatar( + content: _userinfo!.avatar, radius: 16), const Gap(12), Expanded( child: Column( diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index f5486de..4d462e1 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -252,7 +252,7 @@ class _ChatListState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - AccountAvatar( + AttachedCircleAvatar( content: x.avatar, radius: 14, fallbackWidget: const Icon( diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 3e54219..f293d40 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -392,7 +392,7 @@ class _DashboardScreenState extends State { backgroundColor: Theme.of(context) .colorScheme .surfaceContainerLow, - ), + ).paddingAll(8), ), ), ).paddingSymmetric(horizontal: 8), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index ce6bff3..a25fedd 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -6,13 +6,13 @@ import 'package:get/get.dart'; import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/navigation.dart'; -import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/navigation/realm_switcher.dart'; +import 'package:solian/widgets/posts/post_creation.dart'; import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; import 'package:solian/widgets/root_container.dart'; @@ -225,106 +225,3 @@ class _ExploreScreenState extends State super.dispose(); } } - -class PostCreatePopup extends StatelessWidget { - final bool hideDraftBox; - - const PostCreatePopup({ - super.key, - this.hideDraftBox = false, - }); - - @override - Widget build(BuildContext context) { - final AuthProvider auth = Get.find(); - - if (auth.isAuthorized.isFalse) { - return const SizedBox.shrink(); - } - - final List actionList = [ - ( - icon: const Icon(Icons.post_add), - label: 'postEditorModeStory'.tr, - onTap: () { - Navigator.pop( - context, - AppRouter.instance.pushNamed( - 'postEditor', - queryParameters: { - 'mode': 0.toString(), - }, - ), - ); - }, - ), - ( - icon: const Icon(Icons.description), - label: 'postEditorModeArticle'.tr, - onTap: () { - Navigator.pop( - context, - AppRouter.instance.pushNamed( - 'postEditor', - queryParameters: { - 'mode': 1.toString(), - }, - ), - ); - }, - ), - ( - icon: const Icon(Icons.drafts), - label: 'draftBoxOpen'.tr, - onTap: () { - Navigator.pop( - context, - AppRouter.instance.pushNamed('draftBox'), - ); - }, - ), - ]; - - return SizedBox( - height: MediaQuery.of(context).size.height * 0.38, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'postNew'.tr, - style: Theme.of(context).textTheme.headlineSmall, - ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), - Expanded( - child: GridView.count( - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 3, - children: actionList - .map((x) => Card( - color: Theme.of(context).colorScheme.surfaceContainer, - child: InkWell( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - onTap: x.onTap, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - x.icon, - const Gap(8), - Expanded( - child: Text( - x.label, - overflow: TextOverflow.fade, - ), - ), - ], - ).paddingAll(18), - ), - )) - .toList(), - ).paddingSymmetric(horizontal: 20), - ), - ], - ), - ); - } -} diff --git a/lib/screens/posts/post_detail.dart b/lib/screens/posts/post_detail.dart index e686ad4..ba7d70a 100644 --- a/lib/screens/posts/post_detail.dart +++ b/lib/screens/posts/post_detail.dart @@ -24,11 +24,11 @@ class PostDetailScreen extends StatefulWidget { class _PostDetailScreenState extends State { Post? item; - Future getDetail() async { + Future _getDetail() async { if (widget.post != null) { - item = widget.post; - Get.find().feedLastReadAt = item?.id; - return widget.post; + setState(() { + item = widget.post; + }); } final PostProvider provider = Get.find(); @@ -48,7 +48,7 @@ class _PostDetailScreenState extends State { @override Widget build(BuildContext context) { return FutureBuilder( - future: getDetail(), + future: _getDetail(), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data == null) { return const Center( diff --git a/lib/screens/realms.dart b/lib/screens/realms.dart index f23b104..83ddaef 100644 --- a/lib/screens/realms.dart +++ b/lib/screens/realms.dart @@ -156,7 +156,7 @@ class _RealmListScreenState extends State { size: 18, ), ) - : AccountAvatar( + : AttachedCircleAvatar( content: element.avatar!, bgColor: Theme.of(context).colorScheme.primary, ), diff --git a/lib/widgets/account/account_avatar.dart b/lib/widgets/account/account_avatar.dart index 53ad0f5..7c07a4b 100644 --- a/lib/widgets/account/account_avatar.dart +++ b/lib/widgets/account/account_avatar.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:solian/services.dart'; +import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/auto_cache_image.dart'; -class AccountAvatar extends StatelessWidget { +class AttachedCircleAvatar extends StatelessWidget { final dynamic content; final Color? bgColor; final Color? feColor; final double? radius; final Widget? fallbackWidget; - const AccountAvatar({ + const AttachedCircleAvatar({ super.key, required this.content, this.bgColor, @@ -39,7 +40,7 @@ class AccountAvatar extends StatelessWidget { child: isEmpty ? (fallbackWidget ?? Icon( - Icons.account_circle, + Icons.image, size: radius != null ? radius! * 1.2 : 24, color: feColor, )) @@ -48,6 +49,54 @@ class AccountAvatar extends StatelessWidget { } } +class AccountAvatar extends StatelessWidget { + final dynamic content; + final String username; + final Color? bgColor; + final Color? feColor; + final double? radius; + final Widget? fallbackWidget; + + const AccountAvatar({ + super.key, + required this.content, + required this.username, + this.bgColor, + this.feColor, + this.radius, + this.fallbackWidget, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: AttachedCircleAvatar( + content: content, + bgColor: bgColor, + feColor: feColor, + radius: radius, + fallbackWidget: (fallbackWidget ?? + Icon( + Icons.account_circle, + size: radius != null ? radius! * 1.2 : 24, + color: feColor, + )), + ), + onTap: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Theme.of(context).colorScheme.surface, + context: context, + builder: (context) => AccountProfilePopup( + name: username, + ), + ); + }, + ); + } +} + class AccountProfileImage extends StatelessWidget { final dynamic content; final BoxFit fit; diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index 7e7d2f0..b626ad0 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget { Positioned( bottom: -30, left: 32, - child: AccountAvatar(content: avatar, radius: 40), + child: AttachedCircleAvatar(content: avatar, radius: 40), ), ], ), diff --git a/lib/widgets/account/account_select.dart b/lib/widgets/account/account_select.dart index 2328720..464e439 100644 --- a/lib/widgets/account/account_select.dart +++ b/lib/widgets/account/account_select.dart @@ -138,7 +138,7 @@ class _AccountSelectorState extends State { return ListTile( title: Text(element.nick), subtitle: Text(element.name), - leading: AccountAvatar(content: element.avatar), + leading: AttachedCircleAvatar(content: element.avatar), trailing: widget.trailingBuilder != null ? widget.trailingBuilder!(element) : _checkSelected(element) diff --git a/lib/widgets/account/relative_list.dart b/lib/widgets/account/relative_list.dart index 0a8cabc..69f638f 100644 --- a/lib/widgets/account/relative_list.dart +++ b/lib/widgets/account/relative_list.dart @@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget { title: Text(element.related.nick), subtitle: Text(element.related.name), leading: GestureDetector( - child: AccountAvatar(content: element.related.avatar), + child: AttachedCircleAvatar(content: element.related.avatar), onTap: () { showModalBottomSheet( useRootNavigator: true, diff --git a/lib/widgets/account/relative_select.dart b/lib/widgets/account/relative_select.dart index 539d9ea..c804d60 100644 --- a/lib/widgets/account/relative_select.dart +++ b/lib/widgets/account/relative_select.dart @@ -56,7 +56,7 @@ class _RelativeSelectorState extends State { return ListTile( title: Text(element.nick), subtitle: Text(element.name), - leading: AccountAvatar(content: element.avatar), + leading: AttachedCircleAvatar(content: element.avatar), trailing: widget.trailingBuilder != null ? widget.trailingBuilder!(element) : null, diff --git a/lib/widgets/attachments/attachment_fullscreen.dart b/lib/widgets/attachments/attachment_fullscreen.dart index 810f8aa..0a8947e 100644 --- a/lib/widgets/attachments/attachment_fullscreen.dart +++ b/lib/widgets/attachments/attachment_fullscreen.dart @@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State { Row( children: [ IgnorePointer( - child: AccountAvatar( + child: AttachedCircleAvatar( content: widget.item.account!.avatar, radius: 19, ), diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index 8a98ab0..37bb01c 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -19,9 +19,8 @@ class AttachmentList extends StatefulWidget { final List? attachments; final bool isGrid; final bool isColumn; - final bool isForceGrid; + final bool isFullWidth; final bool autoload; - final double flatMaxHeight; final double columnMaxWidth; final double? width; @@ -34,9 +33,8 @@ class AttachmentList extends StatefulWidget { this.attachments, this.isGrid = false, this.isColumn = false, - this.isForceGrid = false, + this.isFullWidth = false, this.autoload = false, - this.flatMaxHeight = 720, this.columnMaxWidth = 480, this.width, this.viewport, @@ -175,9 +173,70 @@ class _AttachmentListState extends State { .fadeIn(duration: 1250.ms); } + const radius = BorderRadius.all(Radius.circular(8)); + + if (widget.isFullWidth && _attachments.length == 1) { + final element = _attachments.first; + double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; + return Container( + constraints: BoxConstraints( + maxWidth: widget.columnMaxWidth, + maxHeight: 640, + ), + child: AspectRatio( + aspectRatio: ratio, + child: Container( + decoration: BoxDecoration( + border: Border.symmetric( + horizontal: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: _buildEntry(element, 0), + ), + ), + ); + } + + final isNotPureImage = _attachments.any( + (x) => x?.mimetype.split('/').firstOrNull != 'image', + ); + if (widget.isGrid && !isNotPureImage) { + return GridView.builder( + padding: EdgeInsets.zero, + primary: false, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: math.min(3, _attachments.length), + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + ), + itemCount: _attachments.length, + itemBuilder: (context, idx) { + final element = _attachments[idx]; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + borderRadius: radius, + ), + child: ClipRRect( + borderRadius: radius, + child: _buildEntry(element, idx), + ), + ); + }, + ); + } + if (widget.isColumn) { var idx = 0; - const radius = BorderRadius.all(Radius.circular(8)); return Wrap( spacing: 8, runSpacing: 8, @@ -212,68 +271,44 @@ class _AttachmentListState extends State { ); } - final isNotPureImage = _attachments.any( - (x) => x?.mimetype.split('/').firstOrNull != 'image', - ); - if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { - const radius = BorderRadius.all(Radius.circular(8)); - return GridView.builder( - padding: EdgeInsets.zero, - primary: false, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: math.min(3, _attachments.length), - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - ), - itemCount: _attachments.length, - itemBuilder: (context, idx) { - final element = _attachments[idx]; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - 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: Colors.transparent, - border: Border.symmetric( - horizontal: BorderSide( - width: 0.3, - color: Theme.of(context).dividerColor, - ), - ), - ), + return SizedBox( + width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth), child: CarouselSlider.builder( options: CarouselOptions( + disableCenter: true, animateToClosest: true, aspectRatio: _aspectRatio, - viewportFraction: - widget.viewport ?? (_attachments.length > 1 ? 0.95 : 1), + enlargeCenterPage: true, + viewportFraction: widget.viewport ?? 0.95, enableInfiniteScroll: false, ), itemCount: _attachments.length, itemBuilder: (context, idx, _) { final element = _attachments[idx]; - return _buildEntry(element, idx); + if (element == null) const SizedBox.shrink(); + double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; + return Container( + constraints: BoxConstraints( + maxWidth: widget.columnMaxWidth, + maxHeight: 640, + ), + child: AspectRatio( + aspectRatio: ratio, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + borderRadius: radius, + ), + child: ClipRRect( + borderRadius: radius, + child: _buildEntry(element, idx), + ), + ), + ), + ); }, ), ); diff --git a/lib/widgets/channel/channel_list.dart b/lib/widgets/channel/channel_list.dart index 398f1ff..d149196 100644 --- a/lib/widgets/channel/channel_list.dart +++ b/lib/widgets/channel/channel_list.dart @@ -205,7 +205,7 @@ class _ChannelListWidgetState extends State { item.members!.where((e) => e.account.id != widget.selfId).firstOrNull; if (item.type == 1 && otherside != null) { - final avatar = AccountAvatar( + final avatar = AttachedCircleAvatar( content: otherside.account.avatar, radius: 20, bgColor: Theme.of(context).colorScheme.primary, @@ -241,7 +241,7 @@ class _ChannelListWidgetState extends State { padding: const EdgeInsets.all(2), elevation: 8, ), - badgeContent: AccountAvatar( + badgeContent: AttachedCircleAvatar( content: item.realm?.avatar, radius: 10, fallbackWidget: const Icon( diff --git a/lib/widgets/channel/channel_member.dart b/lib/widgets/channel/channel_member.dart index 547f078..0753c1b 100644 --- a/lib/widgets/channel/channel_member.dart +++ b/lib/widgets/channel/channel_member.dart @@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State { title: Text(element.account.nick), subtitle: Text(element.account.name), leading: GestureDetector( - child: AccountAvatar(content: element.account.avatar), + child: + AttachedCircleAvatar(content: element.account.avatar), onTap: () { showModalBottomSheet( useRootNavigator: true, diff --git a/lib/widgets/chat/call/call_no_content.dart b/lib/widgets/chat/call/call_no_content.dart index 62db110..1eb5cb8 100644 --- a/lib/widgets/chat/call/call_no_content.dart +++ b/lib/widgets/chat/call/call_no_content.dart @@ -74,7 +74,7 @@ class _NoContentWidgetState extends State ), ) ], - child: AccountAvatar( + child: AttachedCircleAvatar( content: widget.userinfo!.avatar, bgColor: Colors.transparent, radius: radius, diff --git a/lib/widgets/chat/chat_event.dart b/lib/widgets/chat/chat_event.dart index 406d92f..f76d80f 100644 --- a/lib/widgets/chat/chat_event.dart +++ b/lib/widgets/chat/chat_event.dart @@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget { children: [ Row( children: [ - AccountAvatar( + AttachedCircleAvatar( content: item.sender.account.avatar, radius: 9, ), @@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - child: AccountAvatar(content: item.sender.account.avatar), + child: + AttachedCircleAvatar(content: item.sender.account.avatar), onTap: () { showModalBottomSheet( useRootNavigator: true, diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index 64f3d3b..22600c8 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -443,7 +443,7 @@ class _ChatMessageInputState extends State { .map( (x) => ChatMessageSuggestion( type: 'users', - leading: AccountAvatar(content: x.avatar), + leading: AttachedCircleAvatar(content: x.avatar), display: x.nick, content: '@${x.name}', ), diff --git a/lib/widgets/navigation/app_account_widget.dart b/lib/widgets/navigation/app_account_widget.dart index 77defdd..9a7436b 100644 --- a/lib/widgets/navigation/app_account_widget.dart +++ b/lib/widgets/navigation/app_account_widget.dart @@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State { bottom: 0, end: -2, ), - child: AccountAvatar( + child: AttachedCircleAvatar( radius: 14, content: auth.userProfile.value!['avatar'], ), diff --git a/lib/widgets/navigation/realm_switcher.dart b/lib/widgets/navigation/realm_switcher.dart index 8a9787a..2af555c 100644 --- a/lib/widgets/navigation/realm_switcher.dart +++ b/lib/widgets/navigation/realm_switcher.dart @@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (item != null) - AccountAvatar( + AttachedCircleAvatar( content: item.avatar, radius: 14, fallbackWidget: const Icon( diff --git a/lib/widgets/posts/post_creation.dart b/lib/widgets/posts/post_creation.dart new file mode 100644 index 0000000..9f1e481 --- /dev/null +++ b/lib/widgets/posts/post_creation.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; + +class PostCreatePopup extends StatelessWidget { + final bool hideDraftBox; + + const PostCreatePopup({ + super.key, + this.hideDraftBox = false, + }); + + @override + Widget build(BuildContext context) { + final AuthProvider auth = Get.find(); + + if (auth.isAuthorized.isFalse) { + return const SizedBox.shrink(); + } + + final List actionList = [ + ( + icon: const Icon(Icons.post_add), + label: 'postEditorModeStory'.tr, + onTap: () { + Navigator.pop( + context, + AppRouter.instance.pushNamed( + 'postEditor', + queryParameters: { + 'mode': 0.toString(), + }, + ), + ); + }, + ), + ( + icon: const Icon(Icons.description), + label: 'postEditorModeArticle'.tr, + onTap: () { + Navigator.pop( + context, + AppRouter.instance.pushNamed( + 'postEditor', + queryParameters: { + 'mode': 1.toString(), + }, + ), + ); + }, + ), + ( + icon: const Icon(Icons.drafts), + label: 'draftBoxOpen'.tr, + onTap: () { + Navigator.pop( + context, + AppRouter.instance.pushNamed('draftBox'), + ); + }, + ), + ]; + + return SizedBox( + height: MediaQuery.of(context).size.height * 0.38, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'postNew'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Expanded( + child: GridView.count( + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + children: actionList + .map((x) => Card( + color: Theme.of(context).colorScheme.surfaceContainer, + child: InkWell( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + onTap: x.onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + x.icon, + const Gap(8), + Expanded( + child: Text( + x.label, + overflow: TextOverflow.fade, + ), + ), + ], + ).paddingAll(18), + ), + )) + .toList(), + ).paddingSymmetric(horizontal: 20), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index a5af3dd..4bc231c 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -12,7 +12,6 @@ import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/shells/title_shell.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/account/account_avatar.dart'; -import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/link_expansion.dart'; import 'package:solian/widgets/markdown_text_content.dart'; @@ -36,6 +35,7 @@ class PostItem extends StatefulWidget { final bool showFeaturedReply; final String? attachmentParent; final Color? backgroundColor; + final Function? onComment; const PostItem({ super.key, @@ -52,6 +52,7 @@ class PostItem extends StatefulWidget { this.showFeaturedReply = false, this.attachmentParent, this.backgroundColor, + this.onComment, }); @override @@ -92,32 +93,27 @@ class _PostItemState extends State { item: item, ).paddingSymmetric(horizontal: 12), _PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12), - Stack( - children: [ - SizedContainer( - maxWidth: 640, - maxHeight: widget.isFullContent ? double.infinity : 80, - child: _MeasureSize( - onChange: (size) { - setState(() => _contentHeight = size.height); - }, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: MarkdownTextContent( - parentId: 'p${item.id}', - content: item.body['content'], - isAutoWarp: item.type == 'story', - isSelectable: widget.isContentSelectable, - ), - ).paddingOnly( - left: 16, - right: 12, - top: 2, - bottom: hasAttachment ? 4 : 0, - ), + SizedContainer( + maxWidth: 640, + maxHeight: widget.isFullContent ? double.infinity : 80, + child: _MeasureSize( + onChange: (size) { + setState(() => _contentHeight = size.height); + }, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: MarkdownTextContent( + parentId: 'p${item.id}', + content: item.body['content'], + isAutoWarp: item.type == 'story', + isSelectable: widget.isContentSelectable, ), + ).paddingOnly( + left: 12, + right: 12, + bottom: hasAttachment ? 4 : 0, ), - ], + ), ), if (_contentHeight >= 80 && !widget.isFullContent) Opacity( @@ -132,7 +128,7 @@ class _PostItemState extends State { right: 8, top: 4, ), - _PostFooterWidget(item: item).paddingOnly(left: 16), + _PostFooterWidget(item: item).paddingOnly(left: 12), if (attachments.isNotEmpty) Row( children: [ @@ -148,7 +144,7 @@ class _PostItemState extends State { style: TextStyle(color: _unFocusColor), ) ], - ).paddingOnly(left: 16, top: 4), + ).paddingOnly(left: 14, top: 4), ], ); } @@ -162,113 +158,80 @@ class _PostItemState extends State { rid: item.body['thumbnail'], parentId: widget.item.id.toString(), ).paddingOnly(bottom: 4), - Row( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - child: AccountAvatar(content: item.author.avatar), - onTap: () { - showModalBottomSheet( - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: Theme.of(context).colorScheme.surface, - context: context, - builder: (context) => AccountProfilePopup( - name: item.author.name, - ), - ); - }, + _PostHeaderWidget( + isCompact: widget.isCompact, + item: item, ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _PostHeaderWidget( - isCompact: widget.isCompact, - item: item, + _PostHeaderDividerWidget(item: item), + SizedContainer( + maxWidth: 640, + maxHeight: widget.isFullContent ? double.infinity : 320, + child: _MeasureSize( + onChange: (size) { + setState(() => _contentHeight = size.height); + }, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: MarkdownTextContent( + parentId: 'p${item.id}-embed', + content: item.body['content'], + isAutoWarp: item.type == 'story', + isSelectable: widget.isContentSelectable, + isLargeText: + item.type == 'article' && widget.isFullContent, ), - _PostHeaderDividerWidget(item: item), - Stack( - children: [ - SizedContainer( - maxWidth: 640, - maxHeight: - widget.isFullContent ? double.infinity : 320, - child: _MeasureSize( - onChange: (size) { - setState(() => _contentHeight = size.height); - }, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: MarkdownTextContent( - parentId: 'p${item.id}-embed', - content: item.body['content'], - isAutoWarp: item.type == 'story', - isSelectable: widget.isContentSelectable, - isLargeText: item.type == 'article' && - widget.isFullContent, - ).paddingOnly(left: 12, right: 8), - ), - ), - ), - ], - ), - if (_contentHeight >= 320 && !widget.isFullContent) - Opacity( - opacity: 0.8, - child: InkWell(child: Text('readMore'.tr)), - ).paddingOnly( - left: 12, - top: 4, - ), - if (widget.item.replyTo != null && widget.isShowEmbed) - Container( - constraints: const BoxConstraints(maxWidth: 480), - padding: const EdgeInsets.only(top: 4), - child: _PostEmbedWidget( - isClickable: widget.isClickable, - isOverrideEmbedClickable: - widget.isOverrideEmbedClickable, - item: widget.item.replyTo!, - username: widget.item.replyTo!.author.name, - hintText: 'postRepliedNotify', - icon: FontAwesomeIcons.reply, - id: widget.item.replyTo!.id.toString(), - ), - ), - if (widget.item.repostTo != null && widget.isShowEmbed) - Container( - constraints: const BoxConstraints(maxWidth: 480), - padding: const EdgeInsets.only(top: 4), - child: _PostEmbedWidget( - isClickable: widget.isClickable, - isOverrideEmbedClickable: - widget.isOverrideEmbedClickable, - item: widget.item.repostTo!, - username: widget.item.repostTo!.author.name, - hintText: 'postRepostedNotify', - icon: FontAwesomeIcons.retweet, - id: widget.item.repostTo!.id.toString(), - ), - ), - _PostFooterWidget(item: item).paddingOnly(left: 12), - LinkExpansion(content: item.body['content']) - .paddingOnly(top: 4), - ], + ), ), ), + if (_contentHeight >= 320 && !widget.isFullContent) + Opacity( + opacity: 0.8, + child: InkWell(child: Text('readMore'.tr)), + ).paddingOnly(top: 4), + if (widget.item.replyTo != null && widget.isShowEmbed) + Container( + constraints: const BoxConstraints(maxWidth: 480), + padding: const EdgeInsets.only(top: 8), + child: _PostEmbedWidget( + isClickable: widget.isClickable, + isOverrideEmbedClickable: widget.isOverrideEmbedClickable, + item: widget.item.replyTo!, + username: widget.item.replyTo!.author.name, + hintText: 'postRepliedNotify', + icon: FontAwesomeIcons.reply, + id: widget.item.replyTo!.id.toString(), + ), + ), + if (widget.item.repostTo != null && widget.isShowEmbed) + Container( + constraints: const BoxConstraints(maxWidth: 480), + padding: const EdgeInsets.only(top: 8), + child: _PostEmbedWidget( + isClickable: widget.isClickable, + isOverrideEmbedClickable: widget.isOverrideEmbedClickable, + item: widget.item.repostTo!, + username: widget.item.repostTo!.author.name, + hintText: 'postRepostedNotify', + icon: FontAwesomeIcons.retweet, + id: widget.item.repostTo!.id.toString(), + ), + ), + _PostFooterWidget(item: item), + LinkExpansion(content: item.body['content']).paddingOnly(top: 4), ], ).paddingOnly( - top: 10, - bottom: - (attachments.length == 1 && !AppTheme.isLargeScreen(context)) - ? 10 - : 0, right: 16, left: 16, ), _PostAttachmentWidget(item: item), - if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item), + if (widget.showFeaturedReply) + _PostFeaturedReplyWidget(item: item).paddingSymmetric( + horizontal: 12, + ), + if (widget.showFeaturedReply) const Gap(8), if (widget.isShowReply || widget.isReactable) PostQuickAction( isShowReply: widget.isShowReply, @@ -280,19 +243,15 @@ class _PostItemState extends State { (item.metric!.reactionList[symbol] ?? 0) + changes; }); }, + onComment: () { + if (widget.onComment != null) { + widget.onComment!(); + } + }, ).paddingOnly( - top: (attachments.length == 1 && !AppTheme.isLargeScreen(context)) - ? 10 - : 6, - left: - (attachments.length == 1 && !AppTheme.isLargeScreen(context)) - ? 24 - : 60, - right: 16, - bottom: 10, + left: 14, + right: 14, ) - else - const Gap(10), ], ), openBuilder: (_, __) => TitleShell( @@ -317,7 +276,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final isLargeScreen = AppTheme.isLargeScreen(context); final unFocusColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.75); @@ -325,13 +283,10 @@ class _PostFeaturedReplyWidget extends StatelessWidget { return const SizedBox.shrink(); } - final List attachments = item.body['attachments'] is List - ? List.from(item.body['attachments']?.whereType()) - : List.empty(); - return FutureBuilder( - future: - Get.find().listPostFeaturedReply(item.id.toString()), + future: Get.find().listPostFeaturedReply( + item.id.toString(), + ), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return const SizedBox.shrink(); @@ -351,7 +306,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - AccountAvatar( + AttachedCircleAvatar( content: reply.author.avatar, radius: 10, ), @@ -423,16 +378,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget { .toList(), ), ), - ) - .animate() - .fadeIn( + ).animate().fadeIn( duration: 300.ms, curve: Curves.easeIn, - ) - .paddingOnly( - top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6, - left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60, - right: 16, ); }, ); @@ -452,30 +400,33 @@ class _PostAttachmentWidget extends StatelessWidget { ? List.from(item.body['attachments']?.whereType()) : List.empty(); - if (attachments.length > 3) { + if (attachments.isEmpty) return const SizedBox.shrink(); + + if (attachments.length == 1) { + return AttachmentList( + parentId: item.id.toString(), + attachmentIds: item.preload == null ? attachments : null, + attachments: item.preload?.attachments, + autoload: false, + isFullWidth: true, + ).paddingOnly(top: 4); + } else if (attachments.length > 1 && + attachments.length % 3 == 0 && + !isLargeScreen) { return AttachmentList( parentId: item.id.toString(), attachmentIds: item.preload == null ? attachments : null, attachments: item.preload?.attachments, autoload: false, isGrid: true, - ).paddingOnly(left: 36, top: 4, bottom: 4); - } else if (attachments.length > 1 || isLargeScreen) { - return AttachmentList( - parentId: item.id.toString(), - attachmentIds: item.preload == null ? attachments : null, - attachments: item.preload?.attachments, - autoload: false, - isColumn: true, - ).paddingOnly(left: 60, right: 24, top: 4, bottom: 4); + ).paddingSymmetric(horizontal: 14, vertical: 8); } else { return AttachmentList( - flatMaxHeight: MediaQuery.of(context).size.width, parentId: item.id.toString(), attachmentIds: item.preload == null ? attachments : null, attachments: item.preload?.attachments, autoload: false, - ); + ).paddingOnly(bottom: 8, top: 4); } } } @@ -515,16 +466,17 @@ class _PostEmbedWidget extends StatelessWidget { size: 16, color: unFocusColor, ), + const Gap(6), Expanded( child: Text( hintText.trParams( {'username': '@$username'}, ), style: TextStyle(color: unFocusColor), - ).paddingOnly(left: 6), + ), ), ], - ).paddingOnly(left: 12), + ).paddingOnly(left: 2), Card( elevation: 1, child: PostItem( @@ -560,9 +512,7 @@ class _PostHeaderDividerWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (item.body['description'] != null || item.body['title'] != null) { - return const Divider(thickness: 0.3, height: 1).paddingSymmetric( - vertical: 8, - ); + return const Gap(8); } return const SizedBox.shrink(); } @@ -634,48 +584,58 @@ class _PostHeaderWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isCompact) - AccountAvatar( - content: item.author.avatar, - radius: 10, - ).paddingOnly(left: 2, top: 1), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AccountAvatar( + content: item.author.avatar, + username: item.author.name, + radius: isCompact ? 10 : null, + ), + Gap(isCompact ? 6 : 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - item.author.nick, - style: const TextStyle(fontWeight: FontWeight.bold), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + item.author.nick, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (isCompact) const Gap(4), + if (isCompact) + RelativeDate( + item.publishedAt?.toLocal() ?? DateTime.now(), + ).paddingOnly(top: 1), + ], ), - RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()) - .paddingOnly(left: 4), + if (!isCompact) + RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()), ], ), - if (item.body['title'] != null) - Text( - item.body['title'], - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 15), - ), - if (item.body['description'] != null) - Text( - item.body['description'], - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ).paddingOnly(left: isCompact ? 6 : 12), + ), + if (item.type == 'article') + Badge( + label: Text('article'.tr), + ).paddingOnly(top: 3), + ], ), - if (item.type == 'article') - Badge( - label: Text('article'.tr), - ).paddingOnly(top: 3), + const Gap(8), + if (item.body['title'] != null) + Text( + item.body['title'], + style: Theme.of(context).textTheme.titleMedium, + ), + if (item.body['description'] != null) + Text( + item.body['description'], + style: Theme.of(context).textTheme.titleSmall, + ), ], ); } diff --git a/lib/widgets/posts/post_list.dart b/lib/widgets/posts/post_list.dart index 8bd32c9..70d8159 100644 --- a/lib/widgets/posts/post_list.dart +++ b/lib/widgets/posts/post_list.dart @@ -3,6 +3,8 @@ import 'package:get/get.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/models/post.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_item.dart'; @@ -12,6 +14,7 @@ class PostListWidget extends StatelessWidget { final bool isNestedClickable; final PagingController controller; final Color? backgroundColor; + final EdgeInsets? padding; const PostListWidget({ super.key, @@ -20,6 +23,7 @@ class PostListWidget extends StatelessWidget { this.isClickable = true, this.isNestedClickable = true, this.backgroundColor, + this.padding, }); @override @@ -29,16 +33,19 @@ class PostListWidget extends StatelessWidget { pagingController: controller, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { - return PostListEntryWidget( - isShowEmbed: isShowEmbed, - isNestedClickable: isNestedClickable, - isClickable: isClickable, - showFeaturedReply: true, - item: item, - backgroundColor: backgroundColor, - onUpdate: () { - controller.refresh(); - }, + return Padding( + padding: padding ?? EdgeInsets.zero, + child: PostListEntryWidget( + isShowEmbed: isShowEmbed, + isNestedClickable: isNestedClickable, + isClickable: isClickable, + showFeaturedReply: true, + item: item, + backgroundColor: backgroundColor, + onUpdate: () { + controller.refresh(); + }, + ), ); }, ), @@ -77,6 +84,22 @@ class PostListEntryWidget extends StatelessWidget { isClickable: isNestedClickable, showFeaturedReply: showFeaturedReply, backgroundColor: backgroundColor, + onComment: () { + AppRouter.instance + .pushNamed( + 'postEditor', + extra: PostPublishArguments(reply: item), + ) + .then((value) { + if (value is Future) { + value.then((_) { + onUpdate(); + }); + } else if (value != null) { + onUpdate(); + } + }); + }, ).paddingSymmetric(vertical: 8), onLongPress: () { final AuthProvider auth = Get.find(); diff --git a/lib/widgets/posts/post_quick_action.dart b/lib/widgets/posts/post_quick_action.dart index f9dfe34..0ea4668 100644 --- a/lib/widgets/posts/post_quick_action.dart +++ b/lib/widgets/posts/post_quick_action.dart @@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget { final Post item; final bool isReactable; final bool isShowReply; + final Function onComment; final void Function(String symbol, int num) onReact; const PostQuickAction({ @@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget { required this.item, this.isShowReply = true, this.isReactable = true, + required this.onComment, required this.onReact, }); @@ -106,7 +108,11 @@ class _PostQuickActionState extends State { builder: (context) { return PostReplyListPopup(item: widget.item); }, - ); + ).then((signal) { + if (signal == true) { + widget.onComment(); + } + }); }, ), ), diff --git a/lib/widgets/posts/post_replies.dart b/lib/widgets/posts/post_replies.dart index 7de3be8..fdca4f5 100644 --- a/lib/widgets/posts/post_replies.dart +++ b/lib/widgets/posts/post_replies.dart @@ -53,6 +53,7 @@ class _PostReplyListState extends State { @override Widget build(BuildContext context) { return PostListWidget( + padding: EdgeInsets.symmetric(horizontal: 10), isShowEmbed: false, controller: _pagingController, backgroundColor: widget.backgroundColor, @@ -70,16 +71,30 @@ class PostReplyListPopup extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'postReplies'.tr, - style: Theme.of(context).textTheme.headlineSmall, - ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Row( + children: [ + Expanded( + child: Text( + 'postReplies'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + IconButton( + icon: const Icon(Icons.add_comment), + visualDensity: const VisualDensity(horizontal: -4), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ).paddingOnly(left: 24, right: 24, top: 24, bottom: 8), Expanded( child: CustomScrollView( slivers: [ PostReplyList( item: item, - backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerLow, ), ], ), diff --git a/lib/widgets/realms/realm_member.dart b/lib/widgets/realms/realm_member.dart index b659491..386307f 100644 --- a/lib/widgets/realms/realm_member.dart +++ b/lib/widgets/realms/realm_member.dart @@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State { title: Text(element.account.nick), subtitle: Text(element.account.name), leading: GestureDetector( - child: AccountAvatar(content: element.account.avatar), + child: + AttachedCircleAvatar(content: element.account.avatar), onTap: () { showModalBottomSheet( useRootNavigator: true,