✨ Mini editor
This commit is contained in:
parent
5b198412f6
commit
e5239a6ca0
@ -86,6 +86,7 @@
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostDescription": "Description",
|
||||
"postPublish": "Publish",
|
||||
"postPosted": "Post has been posted.",
|
||||
"postPublishedAt": "Published At",
|
||||
"postPublishedUntil": "Published Until",
|
||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
||||
|
@ -92,6 +92,7 @@
|
||||
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
||||
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
|
||||
"postReact": "反应",
|
||||
"postPosted": "帖子已经发表。",
|
||||
"postComments": {
|
||||
"zero": "评论",
|
||||
"one": "{} 条评论",
|
||||
|
@ -234,7 +234,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void post(BuildContext context) async {
|
||||
Future<void> post(BuildContext context) async {
|
||||
if (isBusy || publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@ -294,7 +294,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
data: {
|
||||
'publisher': publisher!.id,
|
||||
'content': contentController.text,
|
||||
'title': titleController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
@ -322,8 +323,6 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -368,6 +367,20 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
titleController.clear();
|
||||
descriptionController.clear();
|
||||
contentController.clear();
|
||||
attachments.clear();
|
||||
editingPost = null;
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
contentController.dispose();
|
||||
|
@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.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_attachment.dart';
|
||||
@ -14,6 +15,7 @@ import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
@ -68,8 +70,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
_fetchPost();
|
||||
}
|
||||
|
||||
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
@ -98,17 +104,47 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(data: _data!),
|
||||
child: PostItem(data: _data!, showComments: false),
|
||||
),
|
||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Text('postCommentsDetailed')
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(_data!.metric.replyCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.padding(horizontal: 16, top: 12, bottom: 4),
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 12),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: _data!.id,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_data != null)
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: _data!.id,
|
||||
),
|
||||
if (_data != null) PostCommentSliverList(parentPostId: _data!.id),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
|
@ -328,7 +328,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
@ -390,7 +390,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () => _writeController.post(context),
|
||||
: () {
|
||||
_writeController.post(context).then((_) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
@ -16,10 +17,10 @@ class PostCommentSliverList extends StatefulWidget {
|
||||
const PostCommentSliverList({super.key, required this.parentPostId});
|
||||
|
||||
@override
|
||||
State<PostCommentSliverList> createState() => _PostCommentSliverListState();
|
||||
State<PostCommentSliverList> createState() => PostCommentSliverListState();
|
||||
}
|
||||
|
||||
class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
bool _isBusy = true;
|
||||
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
@ -67,6 +68,11 @@ class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_posts.clear();
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -97,7 +103,7 @@ class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
}
|
||||
}
|
||||
|
||||
class PostCommentListPopup extends StatelessWidget {
|
||||
class PostCommentListPopup extends StatefulWidget {
|
||||
final int postId;
|
||||
final int commentCount;
|
||||
const PostCommentListPopup({
|
||||
@ -106,8 +112,17 @@ class PostCommentListPopup extends StatelessWidget {
|
||||
this.commentCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostCommentListPopup> createState() => _PostCommentListPopupState();
|
||||
}
|
||||
|
||||
class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -117,14 +132,36 @@ class PostCommentListPopup extends StatelessWidget {
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(commentCount)
|
||||
.plural(widget.commentCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PostCommentSliverList(parentPostId: postId),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: widget.postId,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: widget.postId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,10 +1,236 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class PostMiniEditor extends StatelessWidget {
|
||||
const PostMiniEditor({super.key});
|
||||
class PostMiniEditor extends StatefulWidget {
|
||||
final int? postReplyId;
|
||||
final Function? onPost;
|
||||
const PostMiniEditor({super.key, this.postReplyId, this.onPost});
|
||||
|
||||
@override
|
||||
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||
}
|
||||
|
||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
|
||||
bool _isFetching = false;
|
||||
bool get _isLoading => _isFetching || _writeController.isLoading;
|
||||
|
||||
List<SnPublisher>? _publishers;
|
||||
|
||||
Future<void> _fetchPublishers() async {
|
||||
setState(() => _isFetching = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
_writeController.setPublisher(_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isFetching = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPublishers();
|
||||
_writeController.fetchRelatedPost(
|
||||
context,
|
||||
replying: widget.postReplyId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_writeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'fieldPostPublisher',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
).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);
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _writeController.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(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.launch,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {
|
||||
if (widget.postReplyId != null)
|
||||
'replying': widget.postReplyId.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () {
|
||||
_writeController.post(context).then((_) {
|
||||
if (!context.mounted) return;
|
||||
if (widget.onPost != null) widget.onPost!();
|
||||
context.showSnackbar('postPosted'.tr());
|
||||
_writeController.reset();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
],
|
||||
).padding(left: 12, right: 16, bottom: 4),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user