From fe028860e919a2a3f2a038462ca9ec7199216b86 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 7 Feb 2025 22:35:04 +0800 Subject: [PATCH] :lipstick: Optimize post editors --- assets/translations/en-US.json | 6 +- lib/screens/post/post_editor.dart | 396 +++++++++--------- lib/widgets/account/account_select.dart | 14 +- lib/widgets/chat/chat_message_input.dart | 1 + lib/widgets/post/post_item.dart | 10 +- lib/widgets/post/post_media_pending_list.dart | 15 +- lib/widgets/post/post_meta_editor.dart | 38 +- 7 files changed, 241 insertions(+), 239 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 62ce433..1b37d95 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -166,9 +166,9 @@ "postPosted": "Post has been posted.", "postPublishedAt": "Published At", "postPublishedUntil": "Published Until", - "postEditingNotice": "You're about to editing a post that posted {}.", - "postReplyingNotice": "You're about to reply to a post that posted {}.", - "postRepostingNotice": "You're about to repost a post that posted {}.", + "postEditingNotice": "You're about to editing a post that posted by {}.", + "postReplyingNotice": "You're about to reply to a post that posted by {}.", + "postRepostingNotice": "You're about to repost a post that posted by {}.", "postReact": "React", "postReactions": "Reactions of Post", "postReactionUpvote": { diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 899aee1..6e57a2b 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -125,6 +125,16 @@ class _PostEditorScreenState extends State { }); } + void _showPublisherPopup() { + showModalBottomSheet( + context: context, + builder: (context) => _PostPublisherPopup( + controller: _writeController, + publishers: _publishers, + ), + ); + } + @override void dispose() { _writeController.dispose(); @@ -198,156 +208,42 @@ class _PostEditorScreenState extends State { ), body: Column( children: [ - DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - hint: Text( - 'fieldPostPublisher', - style: TextStyle( - fontSize: 14, - color: Theme.of(context).hintColor, - ), - ).tr(), - items: >[ - ...(_publishers?.map( - (item) => DropdownMenuItem( - enabled: _writeController.editingPost == null, - value: item, - child: Row( - children: [ - AccountImage(content: item.avatar, radius: 16), - const Gap(8), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!), - Text('@${item.name}') - .textStyle(Theme.of(context).textTheme.bodySmall!) - .fontSize(12), - ], - ), - ), - ], - ), - ), - ) ?? - []), - DropdownMenuItem( - value: null, - child: Row( - children: [ - CircleAvatar( - radius: 16, - backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).colorScheme.onSurface, - child: const Icon(Symbols.add), - ), - const Gap(8), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!), - ], - ), - ), - ], + if (_writeController.editingPost != null) + Container( + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 / MediaQuery.of(context).devicePixelRatio, ), ), - ], - value: _writeController.publisher, - onChanged: (SnPublisher? value) { - if (value == null) { - GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { - if (value == true) { - _publishers = null; - _fetchPublishers(); - } - }); - } else { - _writeController.setPublisher(value); - final config = context.read(); - config.prefs.setInt('int_last_publisher_id', value.id); - } - }, - buttonStyleData: const ButtonStyleData( - padding: EdgeInsets.only(right: 16), - height: 48, ), - menuItemStyleData: const MenuItemStyleData( - height: 48, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.edit, size: 16), + const Gap(10), + Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']), + ], ), ), - ), - const Divider(height: 1), Expanded( child: Stack( children: [ SingleChildScrollView( padding: EdgeInsets.only(bottom: 160), - child: Column( - spacing: 8, - children: [ - // Replying Notice - if (_writeController.replyingPost != null) - Column( - children: [ - ExpansionTile( - minTileHeight: 48, - leading: const Icon(Symbols.reply).padding(left: 4), - title: Text('postReplyingNotice') - .fontSize(15) - .tr(args: ['@${_writeController.replyingPost!.publisher.name}']), - children: [PostItem(data: _writeController.replyingPost!)], - ), - const Divider(height: 1), - ], - ), - // Reposting Notice - if (_writeController.repostingPost != null) - Column( - children: [ - ExpansionTile( - minTileHeight: 48, - leading: const Icon(Symbols.forward).padding(left: 4), - title: Text('postRepostingNotice') - .fontSize(15) - .tr(args: ['@${_writeController.repostingPost!.publisher.name}']), - children: [ - PostItem( - data: _writeController.repostingPost!, - ) - ], - ), - const Divider(height: 1), - ], - ), - // Editing Notice - if (_writeController.editingPost != null) - Column( - children: [ - ExpansionTile( - minTileHeight: 48, - leading: const Icon(Symbols.edit_note).padding(left: 4), - title: Text('postEditingNotice') - .fontSize(15) - .tr(args: ['@${_writeController.editingPost!.publisher.name}']), - children: [PostItem(data: _writeController.editingPost!)], - ), - const Divider(height: 1), - ], - ), - // Content Input Area - switch (_writeController.mode) { - 'stories' => _PostStoryEditor(controller: _writeController), - 'articles' => _PostArticleEditor(controller: _writeController), - _ => const Placeholder(), - }, - ], - ), + child: switch (_writeController.mode) { + 'stories' => _PostStoryEditor( + controller: _writeController, + onTapPublisher: _showPublisherPopup, + ), + 'articles' => _PostArticleEditor( + controller: _writeController, + onTapPublisher: _showPublisherPopup, + ), + _ => const Placeholder(), + }, ), if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) Positioned( @@ -492,28 +388,89 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior { }; } +class _PostPublisherPopup extends StatelessWidget { + final PostWriteController controller; + final List? publishers; + + const _PostPublisherPopup({required this.controller, this.publishers}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.face, size: 24), + const Gap(16), + Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + Expanded( + child: ListView.builder( + itemCount: publishers?.length ?? 0, + itemBuilder: (context, idx) { + final publisher = publishers![idx]; + return ListTile( + title: Text(publisher.nick), + subtitle: Text('@${publisher.name}'), + leading: AccountImage(content: publisher.avatar, radius: 18), + onTap: () { + controller.setPublisher(publisher); + Navigator.pop(context, true); + }, + ); + }, + ), + ), + ], + ); + } +} + class _PostStoryEditor extends StatelessWidget { final PostWriteController controller; + final Function? onTapPublisher; - const _PostStoryEditor({required this.controller}); + const _PostStoryEditor({required this.controller, this.onTapPublisher}); @override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints(maxWidth: 640), - child: TextField( - controller: controller.contentController, - maxLines: null, - decoration: InputDecoration( - hintText: 'fieldPostContent'.tr(), - hintStyle: TextStyle(fontSize: 14), - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapPublisher?.call(); + }, + child: AccountImage( + content: controller.publisher?.avatar, + ), + ), ), - border: InputBorder.none, - ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + Expanded( + child: TextField( + controller: controller.contentController, + maxLines: null, + decoration: InputDecoration( + hintText: 'fieldPostContent'.tr(), + hintStyle: TextStyle(fontSize: 14), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + border: InputBorder.none, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(top: 4), + ), + ], ), ); } @@ -521,60 +478,121 @@ class _PostStoryEditor extends StatelessWidget { class _PostArticleEditor extends StatelessWidget { final PostWriteController controller; + final Function? onTapPublisher; - const _PostArticleEditor({required this.controller}); + const _PostArticleEditor({required this.controller, this.onTapPublisher}); @override Widget build(BuildContext context) { + final editorWidgets = [ + Material( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: InkWell( + child: Row( + children: [ + AccountImage(content: controller.publisher?.avatar, radius: 20), + const Gap(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(controller.publisher?.nick ?? 'loading'.tr()).bold(), + Text('@${controller.publisher?.name}'), + ], + ), + ), + ], + ).padding(horizontal: 12, vertical: 8), + onTap: () { + onTapPublisher?.call(); + }, + ), + ), + const Gap(4), + TextField( + controller: controller.titleController, + decoration: InputDecoration( + labelText: 'fieldPostTitle'.tr(), + border: InputBorder.none, + ), + style: Theme.of(context).textTheme.titleLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(4), + TextField( + controller: controller.descriptionController, + decoration: InputDecoration( + labelText: 'fieldPostDescription'.tr(), + border: InputBorder.none, + ), + maxLines: null, + keyboardType: TextInputType.multiline, + style: Theme.of(context).textTheme.bodyLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(8), + ]; + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) { return Container( constraints: const BoxConstraints(maxWidth: 640 * 2 + 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( children: [ - Expanded( - child: TextField( - controller: controller.contentController, - maxLines: null, - decoration: InputDecoration( - hintText: 'fieldPostContent'.tr(), - hintStyle: TextStyle(fontSize: 14), - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, + ...editorWidgets, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: controller.contentController, + maxLines: null, + decoration: InputDecoration( + hintText: 'fieldPostContent'.tr(), + hintStyle: TextStyle(fontSize: 14), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + border: InputBorder.none, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - border: InputBorder.none, ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - ), - const Gap(8), - Expanded( - child: MarkdownTextContent( - content: controller.contentController.text, - ).padding(horizontal: 24), + const Gap(8), + Expanded( + child: MarkdownTextContent( + content: controller.contentController.text, + ).padding(horizontal: 24), + ), + ], ), ], ), ); } - return Container( - constraints: const BoxConstraints(maxWidth: 640), - child: TextField( - controller: controller.contentController, - maxLines: null, - decoration: InputDecoration( - hintText: 'fieldPostContent'.tr(), - hintStyle: TextStyle(fontSize: 14), - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, + return Column( + children: [ + ...editorWidgets, + Container( + padding: const EdgeInsets.only(top: 8), + constraints: const BoxConstraints(maxWidth: 640), + child: TextField( + controller: controller.contentController, + maxLines: null, + decoration: InputDecoration( + hintText: 'fieldPostContent'.tr(), + hintStyle: TextStyle(fontSize: 14), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + border: InputBorder.none, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - border: InputBorder.none, ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), + ], ); } } diff --git a/lib/widgets/account/account_select.dart b/lib/widgets/account/account_select.dart index 6b4820a..dc623df 100644 --- a/lib/widgets/account/account_select.dart +++ b/lib/widgets/account/account_select.dart @@ -1,5 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; @@ -96,10 +98,14 @@ class _AccountSelectState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.title, - style: Theme.of(context).textTheme.headlineSmall, - ).padding(left: 24, right: 24, top: 16, bottom: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.group, size: 24), + const Gap(16), + Text(widget.title, style: Theme.of(context).textTheme.titleLarge), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), Container( color: Theme.of(context).colorScheme.secondaryContainer, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index d03932f..ddd9f37 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -336,6 +336,7 @@ class ChatMessageInputState extends State { : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]), border: InputBorder.none, ), + textInputAction: TextInputAction.send, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onSubmitted: (_) { if (_isBusy) return; diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 6f8e31d..a3ccfb0 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -833,7 +833,7 @@ class _PostContentHeader extends StatelessWidget { onTap: () { showModalBottomSheet( context: context, - builder: (context) => _PostGetInsightSheet(postId: data.id), + builder: (context) => _PostGetInsightPopup(postId: data.id), ); }, ), @@ -1292,16 +1292,16 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> { } } -class _PostGetInsightSheet extends StatefulWidget { +class _PostGetInsightPopup extends StatefulWidget { final int postId; - const _PostGetInsightSheet({required this.postId}); + const _PostGetInsightPopup({required this.postId}); @override - State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState(); + State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState(); } -class _PostGetInsightSheetState extends State<_PostGetInsightSheet> { +class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { String? _response; String? _thinkingProcess; diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index 999ff2d..318e7c5 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -292,7 +292,7 @@ class PostMediaPendingList extends StatelessWidget { constraints: const BoxConstraints(maxHeight: 120), child: Row( children: [ - const Gap(8), + const Gap(16), if (thumbnail != null) ContextMenuArea( contextMenu: _createContextMenu(context, -1, thumbnail!), @@ -337,15 +337,10 @@ class _PostMediaPendingItem extends StatelessWidget { final sn = context.read(); - return Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, - ), - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.surfaceContainer, - ), + return Material( + elevation: 4, + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: Row( diff --git a/lib/widgets/post/post_meta_editor.dart b/lib/widgets/post/post_meta_editor.dart index 85aea48..aba1da2 100644 --- a/lib/widgets/post/post_meta_editor.dart +++ b/lib/widgets/post/post_meta_editor.dart @@ -19,6 +19,7 @@ const Map kPostVisibilityLevel = { class PostMetaEditor extends StatelessWidget { final PostWriteController controller; + const PostMetaEditor({super.key, required this.controller}); Future _selectDate( @@ -87,26 +88,14 @@ class PostMetaEditor extends StatelessWidget { padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8), child: Column( children: [ - TextField( - controller: controller.titleController, - decoration: InputDecoration( - labelText: 'fieldPostTitle'.tr(), - border: UnderlineInputBorder(), - ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ).padding(horizontal: 24), - if (controller.mode == 'articles') const Gap(4), - if (controller.mode == 'articles') + if (controller.mode == 'stories') TextField( - controller: controller.descriptionController, - maxLines: null, + controller: controller.titleController, decoration: InputDecoration( - labelText: 'fieldPostDescription'.tr(), + labelText: 'fieldPostTitle'.tr(), border: UnderlineInputBorder(), ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ).padding(horizontal: 24), const Gap(4), PostTagsField( @@ -133,8 +122,7 @@ class PostMetaEditor extends StatelessWidget { helperMaxLines: 2, border: UnderlineInputBorder(), ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ).padding(horizontal: 24), const Gap(12), ListTile( @@ -182,8 +170,7 @@ class PostMetaEditor extends StatelessWidget { leading: Icon(Symbols.person), trailing: Icon(Symbols.chevron_right), title: Text('postVisibleUsers').tr(), - subtitle: Text('postSelectedUsers') - .plural(controller.visibleUsers.length), + subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length), onTap: () { _selectVisibleUser(context); }, @@ -194,8 +181,7 @@ class PostMetaEditor extends StatelessWidget { leading: Icon(Symbols.person), trailing: Icon(Symbols.chevron_right), title: Text('postInvisibleUsers').tr(), - subtitle: Text('postSelectedUsers') - .plural(controller.invisibleUsers.length), + subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length), onTap: () { _selectInvisibleUser(context); }, @@ -204,9 +190,7 @@ class PostMetaEditor extends StatelessWidget { leading: const Icon(Symbols.event_available), title: Text('postPublishedAt').tr(), subtitle: Text( - controller.publishedAt != null - ? dateFormatter.format(controller.publishedAt!) - : 'unset'.tr(), + controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(), ), trailing: controller.publishedAt != null ? IconButton( @@ -230,9 +214,7 @@ class PostMetaEditor extends StatelessWidget { leading: const Icon(Symbols.event_busy), title: Text('postPublishedUntil').tr(), subtitle: Text( - controller.publishedUntil != null - ? dateFormatter.format(controller.publishedUntil!) - : 'unset'.tr(), + controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(), ), trailing: controller.publishedUntil != null ? IconButton(