diff --git a/lib/controllers/post_editor_controller.dart b/lib/controllers/post_editor_controller.dart index 5157107..c70379c 100644 --- a/lib/controllers/post_editor_controller.dart +++ b/lib/controllers/post_editor_controller.dart @@ -10,6 +10,7 @@ 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_thumbnail.dart'; import 'package:solian/widgets/posts/editor/post_editor_visibility.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -31,6 +32,7 @@ class PostEditorController extends GetxController { Rx publishedUntil = Rx(null); RxList attachments = RxList.empty(growable: true); RxList tags = RxList.empty(growable: true); + Rx thumbnail = Rx(null); RxList visibleUsers = RxList.empty(growable: true); RxList invisibleUsers = RxList.empty(growable: true); @@ -125,6 +127,15 @@ class PostEditorController extends GetxController { ); } + Future editThumbnail(BuildContext context) { + return showDialog( + context: context, + builder: (context) => PostEditorThumbnailDialog( + controller: this, + ), + ); + } + void toggleDraftMode() { isDraft.value = !isDraft.value; } @@ -165,6 +176,7 @@ class PostEditorController extends GetxController { visibleUsers.clear(); invisibleUsers.clear(); visibility.value = 0; + thumbnail.value = 0; publishedAt.value = null; publishedUntil.value = null; isDraft.value = false; @@ -246,6 +258,7 @@ class PostEditorController extends GetxController { 'title': title, 'description': description, 'content': contentController.text, + 'thumbnail': thumbnail.value, 'tags': tags.map((x) => {'alias': x}).toList(), 'attachments': attachments, 'visible_users': visibleUsers, @@ -269,6 +282,7 @@ class PostEditorController extends GetxController { contentController.text = value['content'] ?? ''; attachments.value = value['attachments'].cast() ?? List.empty(); attachments.refresh(); + thumbnail.value = value['thumbnail']; visibility.value = value['visibility']; isDraft.value = value['is_draft']; if (value['visible_users'] != null) { @@ -308,7 +322,8 @@ class PostEditorController extends GetxController { descriptionController.text.isNotEmpty, contentController.text.isNotEmpty, attachments.isNotEmpty, - tags.isNotEmpty + tags.isNotEmpty, + thumbnail.value != null, ].any((x) => x); } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 53c8412..cb1dbc2 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -208,7 +208,12 @@ class PostCreatePopup extends StatelessWidget { children: [ x.icon, const SizedBox(height: 8), - Text(x.label), + Expanded( + child: Text( + x.label, + overflow: TextOverflow.fade, + ), + ), ], ).paddingAll(18), ), diff --git a/lib/screens/posts/post_editor.dart b/lib/screens/posts/post_editor.dart index 73e0235..ea04d90 100644 --- a/lib/screens/posts/post_editor.dart +++ b/lib/screens/posts/post_editor.dart @@ -453,6 +453,23 @@ class _PostPublishScreenState extends State { _editorController.editPublishZone(context); }, ), + IconButton( + icon: Obx(() { + return badges.Badge( + showBadge: + _editorController.thumbnail.value != null, + position: badges.BadgePosition.topEnd( + top: -4, + end: -6, + ), + child: const Icon(Icons.preview), + ); + }), + color: Theme.of(context).colorScheme.primary, + onPressed: () { + _editorController.editThumbnail(context); + }, + ), IconButton( icon: Obx(() { return badges.Badge( @@ -475,14 +492,15 @@ class _PostPublishScreenState extends State { MarkdownToolbar( hideImage: true, useIncludedTextField: false, - backgroundColor: Colors.transparent, + backgroundColor: + Theme.of(context).colorScheme.surface, iconColor: Theme.of(context).colorScheme.onSurface, controller: _editorController.contentController, focusNode: _contentFocusNode, borderRadius: const BorderRadius.all(Radius.circular(20)), width: 40, - ).paddingSymmetric(horizontal: 4), + ).paddingSymmetric(horizontal: 2), ], ).paddingSymmetric(horizontal: 6, vertical: 8), ), diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index dfecfe2..cdbc399 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -119,6 +119,9 @@ const i18nEnglish = { 'postVisibleUsers': 'Visible users', 'postInvisibleUsers': 'Invisible users', 'postOverview': 'Overview', + 'postThumbnail': 'Thumbnail', + 'postThumbnailAttachmentNew': 'Upload thumbnail', + 'postThumbnailAttachment': 'Attachment serial number', 'postPinned': 'Pinned', 'postListNews': 'News', 'postListShuffle': 'Random', @@ -351,7 +354,7 @@ const i18nEnglish = { 'attachmentSaved': 'Attachment saved to your system album.', 'cropImage': 'Crop Image', 'stickerUploader': 'Upload sticker', - 'stickerUploaderAttachmentNew': 'Upload new attachment', + 'stickerUploaderAttachmentNew': 'Upload sticker', 'stickerUploaderAttachment': 'Attachment serial number', 'stickerUploaderPack': 'Sticker pack serial number', 'stickerUploaderPackHint': diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index ce05b45..504b66f 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -113,6 +113,9 @@ const i18nSimplifiedChinese = { 'postVisibleUsers': '可见帖子者', 'postInvisibleUsers': '隐藏帖子者', 'postOverview': '帖子概览', + 'postThumbnail': '帖子缩略图', + 'postThumbnailAttachmentNew': '上传附件作为缩略图', + 'postThumbnailAttachment': '附件序列号', 'postPinned': '已置顶', 'postEditorModeStory': '发个帖子', 'postEditorModeArticle': '撰写文章', @@ -322,7 +325,7 @@ const i18nSimplifiedChinese = { 'attachmentSaved': '附件已保存到系统相册', 'cropImage': '裁剪图片', 'stickerUploader': '上传贴图', - 'stickerUploaderAttachmentNew': '上传附件', + 'stickerUploaderAttachmentNew': '上传附件作为贴图', 'stickerUploaderAttachment': '附件序列号', 'stickerUploaderPack': '贴图包序号', 'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。', diff --git a/lib/widgets/posts/editor/post_editor_thumbnail.dart b/lib/widgets/posts/editor/post_editor_thumbnail.dart new file mode 100644 index 0000000..34ad3a6 --- /dev/null +++ b/lib/widgets/posts/editor/post_editor_thumbnail.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/controllers/post_editor_controller.dart'; +import 'package:solian/widgets/attachments/attachment_editor.dart'; + +class PostEditorThumbnailDialog extends StatefulWidget { + final PostEditorController controller; + + const PostEditorThumbnailDialog({super.key, required this.controller}); + + @override + State createState() => + _PostEditorThumbnailDialogState(); +} + +class _PostEditorThumbnailDialogState extends State { + final TextEditingController _attachmentController = TextEditingController(); + + void _promptUploadNewAttachment() { + showModalBottomSheet( + context: context, + builder: (context) => AttachmentEditorPopup( + usage: 'i.attachment', + singleMode: true, + imageOnly: true, + autoUpload: true, + onAdd: (value) { + setState(() { + _attachmentController.text = value.toString(); + }); + + widget.controller.thumbnail.value = value; + }, + initialAttachments: const [], + onRemove: (_) {}, + ), + ); + } + + @override + void initState() { + _attachmentController.text = + widget.controller.thumbnail.value?.toString() ?? ''; + super.initState(); + } + + @override + void dispose() { + _attachmentController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('postThumbnail'.tr), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text('postThumbnailAttachmentNew'.tr), + contentPadding: const EdgeInsets.only(left: 16, right: 13), + trailing: const Icon(Icons.chevron_right), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + onTap: () { + _promptUploadNewAttachment(); + }, + ), + const SizedBox(height: 8), + TextField( + controller: _attachmentController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + prefixText: '#', + labelText: 'postThumbnailAttachment'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + widget.controller.thumbnail.value = + int.tryParse(_attachmentController.text); + Navigator.pop(context); + }, + child: Text('confirm'.tr), + ), + ], + ); + } +} diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index d08f196..e05a766 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -76,6 +76,30 @@ class _PostItemState extends State { } } + Widget _buildThumbnail() { + if (widget.item.body['thumbnail'] == null) return const SizedBox(); + const radius = BorderRadius.all(Radius.circular(8)); + return AspectRatio( + aspectRatio: 16 / 9, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 0.3, + ), + borderRadius: radius, + ), + child: ClipRRect( + borderRadius: radius, + child: AttachmentSelfContainedEntry( + id: widget.item.body['thumbnail'], + parentId: 'p${item.id}-thumbnail', + ), + ), + ), + ); + } + Widget _buildHeader() { return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -283,6 +307,7 @@ class _PostItemState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), _buildHeader().paddingSymmetric(horizontal: 12), _buildHeaderDivider().paddingSymmetric(horizontal: 12), Stack( @@ -356,6 +381,7 @@ class _PostItemState extends State { closedBuilder: (_, openContainer) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildThumbnail().paddingSymmetric(horizontal: 12, vertical: 4), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -390,7 +416,7 @@ class _PostItemState extends State { setState(() => _contentHeight = size.height); }, child: MarkdownTextContent( - parentId: 'p${item.id}', + parentId: 'p${item.id}-embed', content: item.body['content'], isSelectable: widget.isContentSelectable, ).paddingOnly(left: 12, right: 8),