diff --git a/assets/images/ellipsis.svg b/assets/images/ellipsis.svg new file mode 100644 index 0000000000..eef149715d --- /dev/null +++ b/assets/images/ellipsis.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/constants/resources.dart b/lib/constants/resources.dart index 449187d764..083efd4ead 100644 --- a/lib/constants/resources.dart +++ b/lib/constants/resources.dart @@ -2,6 +2,8 @@ // ************************************************************************** // Auto generated by https://github.com/fluttercandies/assets_generator // ************************************************************************** +// ignore_for_file: constant_identifier_names + class Resources { const Resources._(); static const String assetsCaptchaHtml = 'assets/captcha.html'; @@ -156,6 +158,8 @@ class Resources { static const String assetsImagesEditImageUndoSvg = 'assets/images/edit_image_undo.svg'; + static const String assetsImagesEllipsisSvg = 'assets/images/ellipsis.svg'; + static const String assetsImagesEmojiAnimalSvg = 'assets/images/emoji_animal.svg'; diff --git a/lib/widgets/message/item/image/image_preview_page.dart b/lib/widgets/message/item/image/image_preview_page.dart index 53b571c27b..6a7980e3ab 100644 --- a/lib/widgets/message/item/image/image_preview_page.dart +++ b/lib/widgets/message/item/image/image_preview_page.dart @@ -18,6 +18,7 @@ import '../../../action_button.dart'; import '../../../avatar_view/avatar_view.dart'; import '../../../image.dart'; import '../../../interactive_decorated_box.dart'; +import '../../../menu.dart'; import '../../../toast.dart'; import '../../../user_selector/conversation_selector.dart'; import '../../message.dart'; @@ -356,83 +357,160 @@ class _Bar extends StatelessWidget { ), ], ), - const Spacer(), - ActionButton( - name: Resources.assetsImagesZoomInSvg, - color: context.theme.icon, - size: 20, - onTap: controller.zoomIn, - ), - const SizedBox(width: 14), - ActionButton( - name: Resources.assetsImagesZoomOutSvg, - size: 20, - color: context.theme.icon, - onTap: controller.zoomOut, - ), - const SizedBox(width: 14), - ActionButton( - name: Resources.assetsImagesRotatoSvg, - color: context.theme.icon, - size: 20, - onTap: controller.rotate, - ), - const SizedBox(width: 14), - if (!isTranscriptPage) - ActionButton( - name: Resources.assetsImagesShareSvg, - size: 20, - color: context.theme.icon, - onTap: () async { - final accountServer = context.accountServer; - final result = await showConversationSelector( - context: context, - singleSelect: true, - title: context.l10n.forward, - onlyContact: false, - ); - if (result == null || result.isEmpty) return; - await accountServer.forwardMessage( - message.messageId, - result.first.encryptCategory!, - conversationId: result.first.conversationId, - recipientId: result.first.userId, - ); - }, - ), - if (!isTranscriptPage) const SizedBox(width: 14), - ActionButton( - name: Resources.assetsImagesCopySvg, - color: context.theme.icon, - size: 20, - onTap: () => _copyUrl( - context, - context.accountServer - .convertMessageAbsolutePath(message, isTranscriptPage)), - ), - const SizedBox(width: 14), - ActionButton( - name: Resources.assetsImagesAttachmentDownloadSvg, - color: context.theme.icon, - size: 20, - onTap: () async { - if (message.mediaUrl?.isEmpty ?? true) return; - await saveAs( - context, context.accountServer, message, isTranscriptPage); - }, - ), const SizedBox(width: 14), - ActionButton( - name: Resources.assetsImagesIcCloseBigSvg, - color: context.theme.icon, - size: 20, - onTap: () => Navigator.pop(context), + _Action( + controller: controller, + isTranscriptPage: isTranscriptPage, + message: message, ), const SizedBox(width: 24), ], ); } +class _Action extends StatelessWidget { + const _Action({ + required this.controller, + required this.isTranscriptPage, + required this.message, + }); + + final TransformImageController controller; + final bool isTranscriptPage; + final MessageItem message; + + static const _dividerWidth = 14.0; + static const _divider = SizedBox(width: _dividerWidth); + static const _width = 36; + + @override + Widget build(BuildContext context) { + Future share() async { + final accountServer = context.accountServer; + final result = await showConversationSelector( + context: context, + singleSelect: true, + title: context.l10n.forward, + onlyContact: false, + ); + if (result == null || result.isEmpty) return; + await accountServer.forwardMessage( + message.messageId, + result.first.encryptCategory!, + conversationId: result.first.conversationId, + recipientId: result.first.userId, + ); + } + + Future copy() => _copyUrl( + context, + context.accountServer + .convertMessageAbsolutePath(message, isTranscriptPage)); + + Future download() async { + if (message.mediaUrl?.isEmpty ?? true) return; + await saveAs(context, context.accountServer, message, isTranscriptPage); + } + + final collapsible = [ + if (!isTranscriptPage) + ActionButton( + name: Resources.assetsImagesShareSvg, + size: 20, + color: context.theme.icon, + onTap: share, + ), + ActionButton( + name: Resources.assetsImagesCopySvg, + color: context.theme.icon, + size: 20, + onTap: copy, + ), + ActionButton( + name: Resources.assetsImagesAttachmentDownloadSvg, + color: context.theme.icon, + size: 20, + onTap: download, + ) + ]; + + final close = ActionButton( + name: Resources.assetsImagesIcCloseBigSvg, + color: context.theme.icon, + size: 20, + onTap: () => Navigator.pop(context), + ); + + final common = [ + ActionButton( + name: Resources.assetsImagesZoomInSvg, + color: context.theme.icon, + size: 20, + onTap: controller.zoomIn, + ), + ActionButton( + name: Resources.assetsImagesZoomOutSvg, + size: 20, + color: context.theme.icon, + onTap: controller.zoomOut, + ), + ActionButton( + name: Resources.assetsImagesRotatoSvg, + color: context.theme.icon, + size: 20, + onTap: controller.rotate, + ), + ]; + + final menu = ContextMenuPortalEntry( + buildMenus: () => [ + ContextMenu( + icon: Resources.assetsImagesShareSvg, + title: context.l10n.forward, + onTap: share, + ), + ContextMenu( + icon: Resources.assetsImagesCopySvg, + title: context.l10n.copy, + onTap: copy, + ), + ContextMenu( + icon: Resources.assetsImagesAttachmentDownloadSvg, + title: context.l10n.download, + onTap: download, + ), + ], + child: Builder( + builder: (context) => ActionButton( + name: Resources.assetsImagesEllipsisSvg, + color: context.theme.icon, + size: 20, + onTapUp: (event) => context.sendMenuPosition(event.globalPosition), + ), + ), + ); + + return Expanded( + child: LayoutBuilder(builder: (context, constraints) { + final count = common.length + collapsible.length + 1; + final collapsed = (count * _width + (count - 1) * _dividerWidth) >= + constraints.maxWidth; + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ...common, + if (collapsed) menu, + close, + if (!collapsed) ...collapsible, + ].joinList(_divider), + ); + }), + ); + } +} + class _Item extends HookWidget { const _Item({ required this.message,