💄 Optimize post editors

This commit is contained in:
LittleSheep 2025-02-07 22:35:04 +08:00
parent a2d2ce4d38
commit fe028860e9
7 changed files with 241 additions and 239 deletions

View File

@ -166,9 +166,9 @@
"postPosted": "Post has been posted.", "postPosted": "Post has been posted.",
"postPublishedAt": "Published At", "postPublishedAt": "Published At",
"postPublishedUntil": "Published Until", "postPublishedUntil": "Published Until",
"postEditingNotice": "You're about to editing 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 {}.", "postReplyingNotice": "You're about to reply to a post that posted by {}.",
"postRepostingNotice": "You're about to repost a post that posted {}.", "postRepostingNotice": "You're about to repost a post that posted by {}.",
"postReact": "React", "postReact": "React",
"postReactions": "Reactions of Post", "postReactions": "Reactions of Post",
"postReactionUpvote": { "postReactionUpvote": {

View File

@ -125,6 +125,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}); });
} }
void _showPublisherPopup() {
showModalBottomSheet(
context: context,
builder: (context) => _PostPublisherPopup(
controller: _writeController,
publishers: _publishers,
),
);
}
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
@ -198,156 +208,42 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
body: Column( body: Column(
children: [ children: [
DropdownButtonHideUnderline( if (_writeController.editingPost != null)
child: DropdownButton2<SnPublisher>( Container(
isExpanded: true, padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
hint: Text( decoration: BoxDecoration(
'fieldPostPublisher', border: Border(
style: TextStyle( bottom: BorderSide(
fontSize: 14, color: Theme.of(context).dividerColor,
color: Theme.of(context).hintColor, width: 1 / MediaQuery.of(context).devicePixelRatio,
),
).tr(),
items: <DropdownMenuItem<SnPublisher>>[
...(_publishers?.map(
(item) => DropdownMenuItem<SnPublisher>(
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<SnPublisher>(
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!),
],
),
),
],
), ),
), ),
],
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<ConfigProvider>();
config.prefs.setInt('int_last_publisher_id', value.id);
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(right: 16),
height: 48,
), ),
menuItemStyleData: const MenuItemStyleData( child: Row(
height: 48, 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( Expanded(
child: Stack( child: Stack(
children: [ children: [
SingleChildScrollView( SingleChildScrollView(
padding: EdgeInsets.only(bottom: 160), padding: EdgeInsets.only(bottom: 160),
child: Column( child: switch (_writeController.mode) {
spacing: 8, 'stories' => _PostStoryEditor(
children: [ controller: _writeController,
// Replying Notice onTapPublisher: _showPublisherPopup,
if (_writeController.replyingPost != null) ),
Column( 'articles' => _PostArticleEditor(
children: [ controller: _writeController,
ExpansionTile( onTapPublisher: _showPublisherPopup,
minTileHeight: 48, ),
leading: const Icon(Symbols.reply).padding(left: 4), _ => const Placeholder(),
title: Text('postReplyingNotice') },
.fontSize(15)
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
children: <Widget>[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: <Widget>[
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: <Widget>[PostItem(data: _writeController.editingPost!)],
),
const Divider(height: 1),
],
),
// Content Input Area
switch (_writeController.mode) {
'stories' => _PostStoryEditor(controller: _writeController),
'articles' => _PostArticleEditor(controller: _writeController),
_ => const Placeholder(),
},
],
),
), ),
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
Positioned( Positioned(
@ -492,28 +388,89 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
}; };
} }
class _PostPublisherPopup extends StatelessWidget {
final PostWriteController controller;
final List<SnPublisher>? 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 { class _PostStoryEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher;
const _PostStoryEditor({required this.controller}); const _PostStoryEditor({required this.controller, this.onTapPublisher});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: TextField( child: Row(
controller: controller.contentController, crossAxisAlignment: CrossAxisAlignment.start,
maxLines: null, children: [
decoration: InputDecoration( Material(
hintText: 'fieldPostContent'.tr(), elevation: 1,
hintStyle: TextStyle(fontSize: 14), borderRadius: const BorderRadius.all(Radius.circular(24)),
isCollapsed: true, child: GestureDetector(
contentPadding: const EdgeInsets.symmetric( onTap: () {
horizontal: 16, onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
),
),
), ),
border: InputBorder.none, Expanded(
), child: TextField(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), 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 { class _PostArticleEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher;
const _PostArticleEditor({required this.controller}); const _PostArticleEditor({required this.controller, this.onTapPublisher});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final editorWidgets = <Widget>[
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)) { if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 640 * 2 + 8), constraints: const BoxConstraints(maxWidth: 640 * 2 + 8),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( ...editorWidgets,
child: TextField( Row(
controller: controller.contentController, crossAxisAlignment: CrossAxisAlignment.start,
maxLines: null, children: [
decoration: InputDecoration( Expanded(
hintText: 'fieldPostContent'.tr(), child: TextField(
hintStyle: TextStyle(fontSize: 14), controller: controller.contentController,
isCollapsed: true, maxLines: null,
contentPadding: const EdgeInsets.symmetric( decoration: InputDecoration(
horizontal: 16, 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(
const Gap(8), content: controller.contentController.text,
Expanded( ).padding(horizontal: 24),
child: MarkdownTextContent( ),
content: controller.contentController.text, ],
).padding(horizontal: 24),
), ),
], ],
), ),
); );
} }
return Container( return Column(
constraints: const BoxConstraints(maxWidth: 640), children: [
child: TextField( ...editorWidgets,
controller: controller.contentController, Container(
maxLines: null, padding: const EdgeInsets.only(top: 8),
decoration: InputDecoration( constraints: const BoxConstraints(maxWidth: 640),
hintText: 'fieldPostContent'.tr(), child: TextField(
hintStyle: TextStyle(fontSize: 14), controller: controller.contentController,
isCollapsed: true, maxLines: null,
contentPadding: const EdgeInsets.symmetric( decoration: InputDecoration(
horizontal: 16, 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(), ],
),
); );
} }
} }

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -96,10 +98,14 @@ class _AccountSelectState extends State<AccountSelect> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
widget.title, crossAxisAlignment: CrossAxisAlignment.center,
style: Theme.of(context).textTheme.headlineSmall, children: [
).padding(left: 24, right: 24, top: 16, bottom: 16), 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( Container(
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),

View File

@ -336,6 +336,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]), : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
border: InputBorder.none, border: InputBorder.none,
), ),
textInputAction: TextInputAction.send,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) { onSubmitted: (_) {
if (_isBusy) return; if (_isBusy) return;

View File

@ -833,7 +833,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, 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; final int postId;
const _PostGetInsightSheet({required this.postId}); const _PostGetInsightPopup({required this.postId});
@override @override
State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState(); State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState();
} }
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> { class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
String? _response; String? _response;
String? _thinkingProcess; String? _thinkingProcess;

View File

@ -292,7 +292,7 @@ class PostMediaPendingList extends StatelessWidget {
constraints: const BoxConstraints(maxHeight: 120), constraints: const BoxConstraints(maxHeight: 120),
child: Row( child: Row(
children: [ children: [
const Gap(8), const Gap(16),
if (thumbnail != null) if (thumbnail != null)
ContextMenuArea( ContextMenuArea(
contextMenu: _createContextMenu(context, -1, thumbnail!), contextMenu: _createContextMenu(context, -1, thumbnail!),
@ -337,15 +337,10 @@ class _PostMediaPendingItem extends StatelessWidget {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Container( return Material(
decoration: BoxDecoration( elevation: 4,
border: Border.all( color: Theme.of(context).colorScheme.surfaceContainer,
color: Theme.of(context).dividerColor, borderRadius: BorderRadius.circular(8),
width: 1,
),
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Row( child: Row(

View File

@ -19,6 +19,7 @@ const Map<int, String> kPostVisibilityLevel = {
class PostMetaEditor extends StatelessWidget { class PostMetaEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
const PostMetaEditor({super.key, required this.controller}); const PostMetaEditor({super.key, required this.controller});
Future<DateTime?> _selectDate( Future<DateTime?> _selectDate(
@ -87,26 +88,14 @@ class PostMetaEditor extends StatelessWidget {
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
child: Column( child: Column(
children: [ children: [
TextField( if (controller.mode == 'stories')
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')
TextField( TextField(
controller: controller.descriptionController, controller: controller.titleController,
maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldPostDescription'.tr(), labelText: 'fieldPostTitle'.tr(),
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24), ).padding(horizontal: 24),
const Gap(4), const Gap(4),
PostTagsField( PostTagsField(
@ -133,8 +122,7 @@ class PostMetaEditor extends StatelessWidget {
helperMaxLines: 2, helperMaxLines: 2,
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24), ).padding(horizontal: 24),
const Gap(12), const Gap(12),
ListTile( ListTile(
@ -182,8 +170,7 @@ class PostMetaEditor extends StatelessWidget {
leading: Icon(Symbols.person), leading: Icon(Symbols.person),
trailing: Icon(Symbols.chevron_right), trailing: Icon(Symbols.chevron_right),
title: Text('postVisibleUsers').tr(), title: Text('postVisibleUsers').tr(),
subtitle: Text('postSelectedUsers') subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length),
.plural(controller.visibleUsers.length),
onTap: () { onTap: () {
_selectVisibleUser(context); _selectVisibleUser(context);
}, },
@ -194,8 +181,7 @@ class PostMetaEditor extends StatelessWidget {
leading: Icon(Symbols.person), leading: Icon(Symbols.person),
trailing: Icon(Symbols.chevron_right), trailing: Icon(Symbols.chevron_right),
title: Text('postInvisibleUsers').tr(), title: Text('postInvisibleUsers').tr(),
subtitle: Text('postSelectedUsers') subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length),
.plural(controller.invisibleUsers.length),
onTap: () { onTap: () {
_selectInvisibleUser(context); _selectInvisibleUser(context);
}, },
@ -204,9 +190,7 @@ class PostMetaEditor extends StatelessWidget {
leading: const Icon(Symbols.event_available), leading: const Icon(Symbols.event_available),
title: Text('postPublishedAt').tr(), title: Text('postPublishedAt').tr(),
subtitle: Text( subtitle: Text(
controller.publishedAt != null controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(),
? dateFormatter.format(controller.publishedAt!)
: 'unset'.tr(),
), ),
trailing: controller.publishedAt != null trailing: controller.publishedAt != null
? IconButton( ? IconButton(
@ -230,9 +214,7 @@ class PostMetaEditor extends StatelessWidget {
leading: const Icon(Symbols.event_busy), leading: const Icon(Symbols.event_busy),
title: Text('postPublishedUntil').tr(), title: Text('postPublishedUntil').tr(),
subtitle: Text( subtitle: Text(
controller.publishedUntil != null controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(),
? dateFormatter.format(controller.publishedUntil!)
: 'unset'.tr(),
), ),
trailing: controller.publishedUntil != null trailing: controller.publishedUntil != null
? IconButton( ? IconButton(