diff --git a/assets/locales/en_us.json b/assets/locales/en_us.json index 49894c1..32b4a8d 100644 --- a/assets/locales/en_us.json +++ b/assets/locales/en_us.json @@ -54,6 +54,7 @@ "about": "About", "edit": "Edit", "delete": "Delete", + "insert": "Insert", "settings": "Settings", "settingsNotificationBgService": "Background notification service", "settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.", diff --git a/assets/locales/zh_cn.json b/assets/locales/zh_cn.json index c8a19f7..6fafe7c 100644 --- a/assets/locales/zh_cn.json +++ b/assets/locales/zh_cn.json @@ -14,6 +14,7 @@ "about": "关于", "edit": "编辑", "delete": "删除", + "insert": "插入", "settings": "设置", "settingsNotificationBgService": "常驻通知服务", "settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。", diff --git a/lib/controllers/post_editor_controller.dart b/lib/controllers/post_editor_controller.dart index 580af37..74b171b 100644 --- a/lib/controllers/post_editor_controller.dart +++ b/lib/controllers/post_editor_controller.dart @@ -125,6 +125,21 @@ class PostEditorController extends GetxController { onRemove: (String value) { attachments.remove(value); }, + onInsert: (String str) { + final text = contentController.text; + final selection = contentController.selection; + final newText = text.replaceRange( + selection.start, + selection.end, + str, + ); + contentController.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed( + offset: selection.baseOffset + str.length, + ), + ); + }, ), ); } diff --git a/lib/widgets/attachments/attachment_editor.dart b/lib/widgets/attachments/attachment_editor.dart index 6c043e5..1c09d06 100644 --- a/lib/widgets/attachments/attachment_editor.dart +++ b/lib/widgets/attachments/attachment_editor.dart @@ -33,12 +33,14 @@ class AttachmentEditorPopup extends StatefulWidget { final List? initialAttachments; final void Function(String) onAdd; final void Function(String) onRemove; + final void Function(String)? onInsert; const AttachmentEditorPopup({ super.key, required this.pool, required this.onAdd, required this.onRemove, + this.onInsert, this.singleMode = false, this.imageOnly = false, this.autoUpload = false, @@ -557,6 +559,22 @@ class _AttachmentEditorPopupState extends State { setState(() => _attachments.removeAt(idx)); }, ), + if (widget.onInsert != null) + PopupMenuItem( + child: ListTile( + title: Text('insert'.tr), + leading: const Icon(Icons.insert_link), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + ), + ), + onTap: () { + widget.onInsert!( + '![](solink://attachments/${element.rid})', + ); + Navigator.pop(context); + }, + ), ], ), ], diff --git a/lib/widgets/attachments/attachment_fullscreen.dart b/lib/widgets/attachments/attachment_fullscreen.dart index 5b23c5e..43f55b4 100644 --- a/lib/widgets/attachments/attachment_fullscreen.dart +++ b/lib/widgets/attachments/attachment_fullscreen.dart @@ -287,6 +287,16 @@ class _AttachmentFullScreenState extends State { '${widget.item.metadata?['width']}x${widget.item.metadata?['height']}', style: metaTextStyle, ), + if (widget.item.metadata?['ratio'] != null) + Text( + (widget.item.metadata?['ratio'] as num) + .toStringAsFixed(2), + style: metaTextStyle, + ), + Text( + widget.item.mimetype, + style: metaTextStyle, + ), ], ), ), diff --git a/lib/widgets/markdown_text_content.dart b/lib/widgets/markdown_text_content.dart index e669863..bfd6c5e 100644 --- a/lib/widgets/markdown_text_content.dart +++ b/lib/widgets/markdown_text_content.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -7,7 +6,9 @@ import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:markdown/markdown.dart' as markdown; import 'package:path/path.dart'; +import 'package:solian/models/attachment.dart'; import 'package:solian/providers/stickers.dart'; +import 'package:solian/widgets/attachments/attachment_item.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/auto_cache_image.dart'; import 'package:syntax_highlight/syntax_highlight.dart'; @@ -18,6 +19,7 @@ import 'account/account_profile_popup.dart'; class MarkdownTextContent extends StatefulWidget { final String content; final String parentId; + final List? attachments; final bool isSelectable; final bool isLargeText; final bool isAutoWarp; @@ -26,6 +28,7 @@ class MarkdownTextContent extends StatefulWidget { super.key, required this.content, required this.parentId, + this.attachments, this.isSelectable = false, this.isLargeText = false, this.isAutoWarp = false, @@ -189,18 +192,42 @@ class _MarkdownTextContentState extends State { ), ).paddingSymmetric(vertical: 4); case 'attachments': + final match = widget.attachments + ?.where((x) => x.rid == segments[1]) + .firstOrNull; const radius = BorderRadius.all(Radius.circular(8)); - return LimitedBox( - maxHeight: MediaQuery.of(context).size.width, - child: ClipRRect( - borderRadius: radius, - child: AttachmentSelfContainedEntry( - isDense: true, - parentId: widget.parentId, - rid: segments[1], + if (match != null) { + final isImage = + match.mimetype.split('/').firstOrNull == 'image'; + double ratio = match.metadata?['ratio']?.toDouble() ?? + (isImage ? 1 : 16 / 9); + return LimitedBox( + maxWidth: 480, + maxHeight: 640, + child: AspectRatio( + aspectRatio: ratio, + child: ClipRRect( + borderRadius: radius, + child: AttachmentItem( + parentId: widget.parentId, + item: match, + ), + ), ), - ), - ).paddingSymmetric(vertical: 4); + ).paddingSymmetric(vertical: 4); + } else { + return LimitedBox( + maxHeight: MediaQuery.of(context).size.width, + child: ClipRRect( + borderRadius: radius, + child: AttachmentSelfContainedEntry( + isDense: true, + parentId: widget.parentId, + rid: segments[1], + ), + ), + ).paddingSymmetric(vertical: 4); + } } } return AutoCacheImage( diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 662e66e..de01978 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -110,6 +110,7 @@ class _PostItemState extends State { child: MarkdownTextContent( parentId: 'p${item.id}', content: item.body['content'], + attachments: item.preload?.attachments, isAutoWarp: item.type == 'story', isSelectable: widget.isContentSelectable, ), @@ -131,20 +132,11 @@ class _PostItemState extends State { right: 8, ), if (attachments.isNotEmpty) - Row( - children: [ - Icon( - Icons.file_copy, - size: 15, - color: _unFocusColor, - ).paddingOnly(right: 5), - Text( - 'attachmentHint'.trParams( - {'count': attachments.length.toString()}, - ), - style: TextStyle(color: _unFocusColor), - ) - ], + _PostAttachmentWidget( + item: item, + padding: widget.padding, + isCompact: true, + isNonScrollAttachment: widget.isNonScrollAttachment, ).paddingOnly(left: 14, top: 4), ], ); @@ -173,6 +165,7 @@ class _PostItemState extends State { child: MarkdownTextContent( parentId: 'p${item.id}-embed', content: item.body['content'], + attachments: item.preload?.attachments, isAutoWarp: item.type == 'story', isSelectable: widget.isContentSelectable, ), @@ -220,6 +213,7 @@ class _PostItemState extends State { _PostAttachmentWidget( item: item, padding: widget.padding, + isCompact: item.type == 'article', isNonScrollAttachment: widget.isNonScrollAttachment, ), if (widget.showFeaturedReply) @@ -388,11 +382,13 @@ class _PostAttachmentWidget extends StatelessWidget { final Post item; final EdgeInsets? padding; final bool isNonScrollAttachment; + final bool isCompact; const _PostAttachmentWidget({ required this.item, required this.padding, required this.isNonScrollAttachment, + this.isCompact = false, }); @override @@ -403,8 +399,32 @@ class _PostAttachmentWidget extends StatelessWidget { ? List.from(item.body['attachments']?.whereType()) : List.empty(); + final unFocusColor = + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + if (attachments.isEmpty) return const SizedBox.shrink(); + if (isCompact) { + return Row( + children: [ + Icon( + Icons.file_copy, + size: 13, + color: unFocusColor, + ).paddingOnly(right: 5), + Text( + 'attachmentHint'.trParams( + {'count': attachments.length.toString()}, + ), + style: TextStyle(color: unFocusColor, fontSize: 13), + ) + ], + ).paddingOnly( + left: (padding?.left ?? 0) + 17, + right: (padding?.right ?? 0) + 17, + ); + } + if (attachments.length == 1 && !isLargeScreen) { return AttachmentList( parentId: item.id.toString(), @@ -422,7 +442,10 @@ class _PostAttachmentWidget extends StatelessWidget { attachments: item.preload?.attachments, autoload: false, isGrid: true, - ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14); + ).paddingOnly( + left: (padding?.left ?? 0) + 14, + right: (padding?.right ?? 0) + 14, + ); } else if (attachments.length == 1 || isNonScrollAttachment) { return AttachmentList( parentId: item.id.toString(), @@ -430,7 +453,10 @@ class _PostAttachmentWidget extends StatelessWidget { attachments: item.preload?.attachments, autoload: false, isColumn: true, - ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14); + ).paddingOnly( + left: (padding?.left ?? 0) + 14, + right: (padding?.right ?? 0) + 14, + ); } else { return AttachmentList( parentId: item.id.toString(),