Post with publish at and until

This commit is contained in:
LittleSheep 2024-08-01 15:49:42 +08:00
parent 7655dfdf37
commit c41a71388d
7 changed files with 189 additions and 20 deletions

View File

@ -7,6 +7,7 @@ import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/widgets/attachments/attachment_editor.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_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_overview.dart';
import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart'; import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart';
import 'package:solian/widgets/posts/editor/post_editor_visibility.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?> replyTo = Rx(null);
Rx<Post?> repostTo = Rx(null); Rx<Post?> repostTo = Rx(null);
Rx<Realm?> realmZone = 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<int> attachments = RxList<int>.empty(growable: true);
RxList<String> tags = RxList<String>.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) { Future<void> editAttachment(BuildContext context) {
return showModalBottomSheet( return showModalBottomSheet(
context: context, context: context,
@ -173,6 +185,8 @@ class PostEditorController extends GetxController {
titleController.text = value.body['title'] ?? ''; titleController.text = value.body['title'] ?? '';
descriptionController.text = value.body['description'] ?? ''; descriptionController.text = value.body['description'] ?? '';
contentController.text = value.body['content'] ?? ''; contentController.text = value.body['content'] ?? '';
publishedAt.value = value.publishedAt;
publishedUntil.value = value.publishedUntil;
tags.value = tags.value =
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(); value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty();
tags.refresh(); tags.refresh();
@ -233,6 +247,9 @@ class PostEditorController extends GetxController {
'visible_users': visibleUsers, 'visible_users': visibleUsers,
'invisible_users': invisibleUsers, 'invisible_users': invisibleUsers,
'visibility': visibility.value, 'visibility': visibility.value,
'published_at': publishedAt.value?.toUtc().toIso8601String() ??
DateTime.now().toUtc().toIso8601String(),
'published_until': publishedUntil.value?.toUtc().toIso8601String(),
'is_draft': isDraft.value, 'is_draft': isDraft.value,
if (replyTo.value != null) 'reply_to': replyTo.value!.id, if (replyTo.value != null) 'reply_to': replyTo.value!.id,
if (repostTo.value != null) 'repost_to': repostTo.value!.id, if (repostTo.value != null) 'repost_to': repostTo.value!.id,
@ -256,6 +273,12 @@ class PostEditorController extends GetxController {
if (value['invisible_users'] != null) { if (value['invisible_users'] != null) {
invisibleUsers.value = value['invisible_users'].cast<int>(); 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) { if (value['reply_to'] != null) {
replyTo.value = Post.fromJson(value['reply_to']); replyTo.value = Post.fromJson(value['reply_to']);
} }

View File

@ -20,6 +20,7 @@ class Post {
Post? repostTo; Post? repostTo;
Realm? realm; Realm? realm;
DateTime? publishedAt; DateTime? publishedAt;
DateTime? publishedUntil;
DateTime? pinnedAt; DateTime? pinnedAt;
bool? isDraft; bool? isDraft;
int authorId; int authorId;
@ -44,6 +45,7 @@ class Post {
required this.repostTo, required this.repostTo,
required this.realm, required this.realm,
required this.publishedAt, required this.publishedAt,
required this.publishedUntil,
required this.pinnedAt, required this.pinnedAt,
required this.isDraft, required this.isDraft,
required this.authorId, required this.authorId,
@ -80,6 +82,9 @@ class Post {
publishedAt: json['published_at'] != null publishedAt: json['published_at'] != null
? DateTime.parse(json['published_at']) ? DateTime.parse(json['published_at'])
: null, : null,
publishedUntil: json['published_until'] != null
? DateTime.parse(json['published_until'])
: null,
pinnedAt: json['pinned_at'] != null pinnedAt: json['pinned_at'] != null
? DateTime.parse(json['pinned_at']) ? DateTime.parse(json['pinned_at'])
: null, : null,
@ -108,6 +113,7 @@ class Post {
'repost_to': repostTo?.toJson(), 'repost_to': repostTo?.toJson(),
'realm': realm?.toJson(), 'realm': realm?.toJson(),
'published_at': publishedAt?.toIso8601String(), 'published_at': publishedAt?.toIso8601String(),
'published_until': publishedUntil?.toIso8601String(),
'pinned_at': pinnedAt?.toIso8601String(), 'pinned_at': pinnedAt?.toIso8601String(),
'is_draft': isDraft, 'is_draft': isDraft,
'author_id': authorId, 'author_id': authorId,

View File

@ -34,7 +34,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
bool _isBusy = false; bool _isBusy = false;
void selectBirthday() async { void _selectBirthday() async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context,
initialDate: _birthday?.toLocal(), initialDate: _birthday?.toLocal(),
@ -49,7 +49,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
} }
} }
void syncWidget() async { void _syncWidget() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final AuthProvider auth = Get.find(); 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(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -87,11 +87,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
try { try {
final file = File(image.path); final file = File(image.path);
attachResp = await provider.createAttachment( attachResp = await provider.createAttachment(
await file.readAsBytes(), await file.readAsBytes(), file.path, 'p.$position', null);
file.path,
'p.$position',
null
);
} catch (e) { } catch (e) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
context.showErrorDialog(e); context.showErrorDialog(e);
@ -105,7 +101,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
{'attachment': attachResp.body['id']}, {'attachment': attachResp.body['id']},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
syncWidget(); _syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr); context.showSnackbar('accountPersonalizeApplied'.tr);
} else { } else {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
@ -114,7 +110,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void updatePersonalize() async { void _editUserInfo() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@ -134,7 +130,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
}, },
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
syncWidget(); _syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr); context.showSnackbar('accountPersonalizeApplied'.tr);
} else { } else {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
@ -147,7 +143,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
void initState() { void initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () => syncWidget()); Future.delayed(Duration.zero, () => _syncWidget());
} }
@override @override
@ -168,7 +164,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
left: 40, left: 40,
child: FloatingActionButton.small( child: FloatingActionButton.small(
heroTag: const Key('avatar-editor'), heroTag: const Key('avatar-editor'),
onPressed: () => updateImage('avatar'), onPressed: () => _editImage('avatar'),
child: const Icon( child: const Icon(
Icons.camera, Icons.camera,
), ),
@ -187,7 +183,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null child: _banner != null
? Image.network( ? Image.network(
ServiceFinder.buildUrl('files', '/attachments/$_banner'), ServiceFinder.buildUrl(
'files', '/attachments/$_banner'),
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child, loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) { ImageChunkEvent? loadingProgress) {
@ -212,7 +209,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
right: 16, right: 16,
child: FloatingActionButton( child: FloatingActionButton(
heroTag: const Key('banner-editor'), heroTag: const Key('banner-editor'),
onPressed: () => updateImage('banner'), onPressed: () => _editImage('banner'),
child: const Icon( child: const Icon(
Icons.camera_alt, Icons.camera_alt,
), ),
@ -293,18 +290,18 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'birthday'.tr, labelText: 'birthday'.tr,
), ),
onTap: () => selectBirthday(), onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding), ).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => syncWidget(), onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr), child: Text('reset'.tr),
), ),
ElevatedButton( ElevatedButton(
onPressed: _isBusy ? null : () => updatePersonalize(), onPressed: _isBusy ? null : () => _editUserInfo(),
child: Text('apply'.tr), child: Text('apply'.tr),
), ),
], ],

View File

@ -82,7 +82,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void syncWidget() { void _syncWidget() {
_editorController.mode.value = widget.mode; _editorController.mode.value = widget.mode;
if (widget.edit != null) { if (widget.edit != null) {
_editorController.editTarget = widget.edit; _editorController.editTarget = widget.edit;
@ -105,7 +105,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_editorController.contentController.addListener(() => setState(() {})); _editorController.contentController.addListener(() => setState(() {}));
syncWidget(); _syncWidget();
} }
@override @override
@ -418,6 +418,25 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
_editorController.editPublishZone(context); _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( MarkdownToolbar(
hideImage: true, hideImage: true,
useIncludedTextField: false, useIncludedTextField: false,

View File

@ -101,6 +101,9 @@ const i18nEnglish = {
'postRestoreFromLocal': 'Restore from local', 'postRestoreFromLocal': 'Restore from local',
'postAutoSaveAt': 'Auto saved at @date', 'postAutoSaveAt': 'Auto saved at @date',
'postCategoriesAndTags': 'Categories n\' Tags', 'postCategoriesAndTags': 'Categories n\' Tags',
'postPublishDate': 'Publish Date',
'postPublishAt': 'Publish At',
'postPublishedUntil': 'Publish Until',
'postPublishZone': 'Publish Zone', 'postPublishZone': 'Publish Zone',
'postPublishZoneNone': 'None', 'postPublishZoneNone': 'None',
'postVisibility': 'Visibility', 'postVisibility': 'Visibility',

View File

@ -95,6 +95,9 @@ const i18nSimplifiedChinese = {
'postRestoreFromLocal': '内容从本地暂存回复', 'postRestoreFromLocal': '内容从本地暂存回复',
'postAutoSaveAt': '已自动保存于 @date', 'postAutoSaveAt': '已自动保存于 @date',
'postCategoriesAndTags': '分类与标签', 'postCategoriesAndTags': '分类与标签',
'postPublishDate': '发布时间',
'postPublishAt': '发布帖子于',
'postPublishedUntil': '取消发布于',
'postPublishZone': '帖子发布区', 'postPublishZone': '帖子发布区',
'postPublishZoneNone': '无所属领域', 'postPublishZoneNone': '无所属领域',
'postVisibility': '帖子可见性', 'postVisibility': '帖子可见性',

View 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),
),
],
);
}
}