✨ Post with publish at and until
This commit is contained in:
parent
7655dfdf37
commit
c41a71388d
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user