✨ Post with publish at and until
This commit is contained in:
		| @@ -7,6 +7,7 @@ import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/models/realm.dart'; | ||||
| import 'package:solian/widgets/attachments/attachment_editor.dart'; | ||||
| import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart'; | ||||
| import 'package:solian/widgets/posts/editor/post_editor_date.dart'; | ||||
| import 'package:solian/widgets/posts/editor/post_editor_overview.dart'; | ||||
| import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart'; | ||||
| import 'package:solian/widgets/posts/editor/post_editor_visibility.dart'; | ||||
| @@ -26,6 +27,8 @@ class PostEditorController extends GetxController { | ||||
|   Rx<Post?> replyTo = Rx(null); | ||||
|   Rx<Post?> repostTo = Rx(null); | ||||
|   Rx<Realm?> realmZone = Rx(null); | ||||
|   Rx<DateTime?> publishedAt = Rx(null); | ||||
|   Rx<DateTime?> publishedUntil = Rx(null); | ||||
|   RxList<int> attachments = RxList<int>.empty(growable: true); | ||||
|   RxList<String> tags = RxList<String>.empty(growable: true); | ||||
|  | ||||
| @@ -98,6 +101,15 @@ class PostEditorController extends GetxController { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> editPublishDate(BuildContext context) { | ||||
|     return showDialog( | ||||
|       context: context, | ||||
|       builder: (context) => PostEditorDateDialog( | ||||
|         controller: this, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> editAttachment(BuildContext context) { | ||||
|     return showModalBottomSheet( | ||||
|       context: context, | ||||
| @@ -173,6 +185,8 @@ class PostEditorController extends GetxController { | ||||
|     titleController.text = value.body['title'] ?? ''; | ||||
|     descriptionController.text = value.body['description'] ?? ''; | ||||
|     contentController.text = value.body['content'] ?? ''; | ||||
|     publishedAt.value = value.publishedAt; | ||||
|     publishedUntil.value = value.publishedUntil; | ||||
|     tags.value = | ||||
|         value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(); | ||||
|     tags.refresh(); | ||||
| @@ -233,6 +247,9 @@ class PostEditorController extends GetxController { | ||||
|       'visible_users': visibleUsers, | ||||
|       'invisible_users': invisibleUsers, | ||||
|       'visibility': visibility.value, | ||||
|       'published_at': publishedAt.value?.toUtc().toIso8601String() ?? | ||||
|           DateTime.now().toUtc().toIso8601String(), | ||||
|       'published_until': publishedUntil.value?.toUtc().toIso8601String(), | ||||
|       'is_draft': isDraft.value, | ||||
|       if (replyTo.value != null) 'reply_to': replyTo.value!.id, | ||||
|       if (repostTo.value != null) 'repost_to': repostTo.value!.id, | ||||
| @@ -256,6 +273,12 @@ class PostEditorController extends GetxController { | ||||
|     if (value['invisible_users'] != null) { | ||||
|       invisibleUsers.value = value['invisible_users'].cast<int>(); | ||||
|     } | ||||
|     if (value['published_at'] != null) { | ||||
|       publishedAt.value = DateTime.parse(value['published_at']).toLocal(); | ||||
|     } | ||||
|     if (value['published_until'] != null) { | ||||
|       publishedAt.value = DateTime.parse(value['published_until']).toLocal(); | ||||
|     } | ||||
|     if (value['reply_to'] != null) { | ||||
|       replyTo.value = Post.fromJson(value['reply_to']); | ||||
|     } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ class Post { | ||||
|   Post? repostTo; | ||||
|   Realm? realm; | ||||
|   DateTime? publishedAt; | ||||
|   DateTime? publishedUntil; | ||||
|   DateTime? pinnedAt; | ||||
|   bool? isDraft; | ||||
|   int authorId; | ||||
| @@ -44,6 +45,7 @@ class Post { | ||||
|     required this.repostTo, | ||||
|     required this.realm, | ||||
|     required this.publishedAt, | ||||
|     required this.publishedUntil, | ||||
|     required this.pinnedAt, | ||||
|     required this.isDraft, | ||||
|     required this.authorId, | ||||
| @@ -80,6 +82,9 @@ class Post { | ||||
|         publishedAt: json['published_at'] != null | ||||
|             ? DateTime.parse(json['published_at']) | ||||
|             : null, | ||||
|         publishedUntil: json['published_until'] != null | ||||
|             ? DateTime.parse(json['published_until']) | ||||
|             : null, | ||||
|         pinnedAt: json['pinned_at'] != null | ||||
|             ? DateTime.parse(json['pinned_at']) | ||||
|             : null, | ||||
| @@ -108,6 +113,7 @@ class Post { | ||||
|         'repost_to': repostTo?.toJson(), | ||||
|         'realm': realm?.toJson(), | ||||
|         'published_at': publishedAt?.toIso8601String(), | ||||
|         'published_until': publishedUntil?.toIso8601String(), | ||||
|         'pinned_at': pinnedAt?.toIso8601String(), | ||||
|         'is_draft': isDraft, | ||||
|         'author_id': authorId, | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   void selectBirthday() async { | ||||
|   void _selectBirthday() async { | ||||
|     final DateTime? picked = await showDatePicker( | ||||
|       context: context, | ||||
|       initialDate: _birthday?.toLocal(), | ||||
| @@ -49,7 +49,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void syncWidget() async { | ||||
|   void _syncWidget() async { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final AuthProvider auth = Get.find(); | ||||
| @@ -72,7 +72,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> updateImage(String position) async { | ||||
|   Future<void> _editImage(String position) async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (auth.isAuthorized.isFalse) return; | ||||
|  | ||||
| @@ -87,11 +87,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|     try { | ||||
|       final file = File(image.path); | ||||
|       attachResp = await provider.createAttachment( | ||||
|         await file.readAsBytes(), | ||||
|         file.path, | ||||
|         'p.$position', | ||||
|         null | ||||
|       ); | ||||
|           await file.readAsBytes(), file.path, 'p.$position', null); | ||||
|     } catch (e) { | ||||
|       setState(() => _isBusy = false); | ||||
|       context.showErrorDialog(e); | ||||
| @@ -105,7 +101,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|       {'attachment': attachResp.body['id']}, | ||||
|     ); | ||||
|     if (resp.statusCode == 200) { | ||||
|       syncWidget(); | ||||
|       _syncWidget(); | ||||
|       context.showSnackbar('accountPersonalizeApplied'.tr); | ||||
|     } else { | ||||
|       context.showErrorDialog(resp.bodyString); | ||||
| @@ -114,7 +110,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|     setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   void updatePersonalize() async { | ||||
|   void _editUserInfo() async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (auth.isAuthorized.isFalse) return; | ||||
|  | ||||
| @@ -134,7 +130,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|       }, | ||||
|     ); | ||||
|     if (resp.statusCode == 200) { | ||||
|       syncWidget(); | ||||
|       _syncWidget(); | ||||
|       context.showSnackbar('accountPersonalizeApplied'.tr); | ||||
|     } else { | ||||
|       context.showErrorDialog(resp.bodyString); | ||||
| @@ -147,7 +143,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|     Future.delayed(Duration.zero, () => syncWidget()); | ||||
|     Future.delayed(Duration.zero, () => _syncWidget()); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -168,7 +164,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|                 left: 40, | ||||
|                 child: FloatingActionButton.small( | ||||
|                   heroTag: const Key('avatar-editor'), | ||||
|                   onPressed: () => updateImage('avatar'), | ||||
|                   onPressed: () => _editImage('avatar'), | ||||
|                   child: const Icon( | ||||
|                     Icons.camera, | ||||
|                   ), | ||||
| @@ -187,7 +183,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                     child: _banner != null | ||||
|                         ? Image.network( | ||||
|                             ServiceFinder.buildUrl('files', '/attachments/$_banner'), | ||||
|                             ServiceFinder.buildUrl( | ||||
|                                 'files', '/attachments/$_banner'), | ||||
|                             fit: BoxFit.cover, | ||||
|                             loadingBuilder: (BuildContext context, Widget child, | ||||
|                                 ImageChunkEvent? loadingProgress) { | ||||
| @@ -212,7 +209,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|                 right: 16, | ||||
|                 child: FloatingActionButton( | ||||
|                   heroTag: const Key('banner-editor'), | ||||
|                   onPressed: () => updateImage('banner'), | ||||
|                   onPressed: () => _editImage('banner'), | ||||
|                   child: const Icon( | ||||
|                     Icons.camera_alt, | ||||
|                   ), | ||||
| @@ -293,18 +290,18 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> { | ||||
|               border: const OutlineInputBorder(), | ||||
|               labelText: 'birthday'.tr, | ||||
|             ), | ||||
|             onTap: () => selectBirthday(), | ||||
|             onTap: () => _selectBirthday(), | ||||
|           ).paddingSymmetric(horizontal: padding), | ||||
|           const SizedBox(height: 16), | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               TextButton( | ||||
|                 onPressed: _isBusy ? null : () => syncWidget(), | ||||
|                 onPressed: _isBusy ? null : () => _syncWidget(), | ||||
|                 child: Text('reset'.tr), | ||||
|               ), | ||||
|               ElevatedButton( | ||||
|                 onPressed: _isBusy ? null : () => updatePersonalize(), | ||||
|                 onPressed: _isBusy ? null : () => _editUserInfo(), | ||||
|                 child: Text('apply'.tr), | ||||
|               ), | ||||
|             ], | ||||
|   | ||||
| @@ -82,7 +82,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|     setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   void syncWidget() { | ||||
|   void _syncWidget() { | ||||
|     _editorController.mode.value = widget.mode; | ||||
|     if (widget.edit != null) { | ||||
|       _editorController.editTarget = widget.edit; | ||||
| @@ -105,7 +105,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _editorController.contentController.addListener(() => setState(() {})); | ||||
|     syncWidget(); | ||||
|     _syncWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -418,6 +418,25 @@ class _PostPublishScreenState extends State<PostPublishScreen> { | ||||
|                             _editorController.editPublishZone(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                           icon: Obx(() { | ||||
|                             return badges.Badge( | ||||
|                               showBadge: | ||||
|                                   _editorController.publishedAt.value != null || | ||||
|                                       _editorController.publishedUntil.value != | ||||
|                                           null, | ||||
|                               position: badges.BadgePosition.topEnd( | ||||
|                                 top: -4, | ||||
|                                 end: -6, | ||||
|                               ), | ||||
|                               child: const Icon(Icons.schedule), | ||||
|                             ); | ||||
|                           }), | ||||
|                           color: Theme.of(context).colorScheme.primary, | ||||
|                           onPressed: () { | ||||
|                             _editorController.editPublishDate(context); | ||||
|                           }, | ||||
|                         ), | ||||
|                         MarkdownToolbar( | ||||
|                           hideImage: true, | ||||
|                           useIncludedTextField: false, | ||||
|   | ||||
| @@ -101,6 +101,9 @@ const i18nEnglish = { | ||||
|   'postRestoreFromLocal': 'Restore from local', | ||||
|   'postAutoSaveAt': 'Auto saved at @date', | ||||
|   'postCategoriesAndTags': 'Categories n\' Tags', | ||||
|   'postPublishDate': 'Publish Date', | ||||
|   'postPublishAt': 'Publish At', | ||||
|   'postPublishedUntil': 'Publish Until', | ||||
|   'postPublishZone': 'Publish Zone', | ||||
|   'postPublishZoneNone': 'None', | ||||
|   'postVisibility': 'Visibility', | ||||
|   | ||||
| @@ -95,6 +95,9 @@ const i18nSimplifiedChinese = { | ||||
|   'postRestoreFromLocal': '内容从本地暂存回复', | ||||
|   'postAutoSaveAt': '已自动保存于 @date', | ||||
|   'postCategoriesAndTags': '分类与标签', | ||||
|   'postPublishDate': '发布时间', | ||||
|   'postPublishAt': '发布帖子于', | ||||
|   'postPublishedUntil': '取消发布于', | ||||
|   'postPublishZone': '帖子发布区', | ||||
|   'postPublishZoneNone': '无所属领域', | ||||
|   'postVisibility': '帖子可见性', | ||||
|   | ||||
							
								
								
									
										118
									
								
								lib/widgets/posts/editor/post_editor_date.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								lib/widgets/posts/editor/post_editor_date.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:solian/controllers/post_editor_controller.dart'; | ||||
|  | ||||
| class PostEditorDateDialog extends StatefulWidget { | ||||
|   final PostEditorController controller; | ||||
|  | ||||
|   const PostEditorDateDialog({super.key, required this.controller}); | ||||
|  | ||||
|   @override | ||||
|   State<PostEditorDateDialog> createState() => _PostEditorDateDialogState(); | ||||
| } | ||||
|  | ||||
| class _PostEditorDateDialogState extends State<PostEditorDateDialog> { | ||||
|   final TextEditingController _publishedAtController = TextEditingController(); | ||||
|   final TextEditingController _publishedUntilController = | ||||
|       TextEditingController(); | ||||
|  | ||||
|   final _dateFormatter = DateFormat('yyyy-MM-dd HH:mm:ss'); | ||||
|  | ||||
|   void _selectDate(int mode) async { | ||||
|     final initial = mode == 0 | ||||
|         ? widget.controller.publishedAt.value | ||||
|         : widget.controller.publishedUntil.value; | ||||
|     final DateTime? pickedDate = await showDatePicker( | ||||
|       context: context, | ||||
|       initialDate: initial?.toLocal(), | ||||
|       firstDate: DateTime(DateTime.now().year), | ||||
|       lastDate: DateTime(DateTime.now().year + 5), | ||||
|     ); | ||||
|     if (pickedDate == null) return; | ||||
|     final TimeOfDay? pickedTime = await showTimePicker( | ||||
|       context: context, | ||||
|       initialTime: TimeOfDay.now(), | ||||
|     ); | ||||
|     if (pickedTime == null) return; | ||||
|     final picked = pickedDate.copyWith( | ||||
|       hour: pickedTime.hour, | ||||
|       minute: pickedTime.minute, | ||||
|     ); | ||||
|     if (mode == 0) { | ||||
|       setState(() { | ||||
|         widget.controller.publishedAt.value = picked; | ||||
|         _publishedAtController.text = _dateFormatter.format(picked); | ||||
|       }); | ||||
|     } else { | ||||
|       widget.controller.publishedUntil.value = pickedDate; | ||||
|       _publishedUntilController.text = _dateFormatter.format(picked); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if (widget.controller.publishedAt.value != null) { | ||||
|       _publishedAtController.text = | ||||
|           _dateFormatter.format(widget.controller.publishedAt.value!); | ||||
|     } | ||||
|     if (widget.controller.publishedUntil.value != null) { | ||||
|       _publishedUntilController.text = | ||||
|           _dateFormatter.format(widget.controller.publishedUntil.value!); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|     _publishedAtController.dispose(); | ||||
|     _publishedUntilController.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text('postPublishDate'.tr), | ||||
|       content: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           TextField( | ||||
|             controller: _publishedAtController, | ||||
|             readOnly: true, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               labelText: 'postPublishAt'.tr, | ||||
|             ), | ||||
|             onTap: () => _selectDate(0), | ||||
|           ), | ||||
|           const SizedBox(height: 16), | ||||
|           TextField( | ||||
|             controller: _publishedUntilController, | ||||
|             readOnly: true, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               labelText: 'postPublishedUntil'.tr, | ||||
|             ), | ||||
|             onTap: () => _selectDate(1), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: () { | ||||
|             widget.controller.publishedAt.value = null; | ||||
|             widget.controller.publishedUntil.value = null; | ||||
|             _publishedAtController.clear(); | ||||
|             _publishedUntilController.clear(); | ||||
|           }, | ||||
|           child: Text('clear'.tr), | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: () => Navigator.pop(context), | ||||
|           child: Text('confirm'.tr), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user