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/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']);
}

View File

@ -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,

View File

@ -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),
),
],

View File

@ -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,

View File

@ -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',

View File

@ -95,6 +95,9 @@ const i18nSimplifiedChinese = {
'postRestoreFromLocal': '内容从本地暂存回复',
'postAutoSaveAt': '已自动保存于 @date',
'postCategoriesAndTags': '分类与标签',
'postPublishDate': '发布时间',
'postPublishAt': '发布帖子于',
'postPublishedUntil': '取消发布于',
'postPublishZone': '帖子发布区',
'postPublishZoneNone': '无所属领域',
'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),
),
],
);
}
}