diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c3eae0523..6bba787fc 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2806,5 +2806,11 @@ "name": "Name", "version": "Version", "website": "Website", - "sendUncompressed": "Send uncompressed" + "sendUncompressed": "Send uncompressed", + "boldText": "Bold text", + "italicText": "Italic text", + "strikeThrough": "Strikethrough", + "pleaseFillOut": "Please fill out", + "invalidUrl": "Invalid url", + "addLink": "Add link" } diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 1d34c7055..f203c3d41 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -9,6 +9,7 @@ import 'package:pasteboard/pasteboard.dart'; import 'package:slugify/slugify.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/markdown_context_builder.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import '../../widgets/avatar.dart'; @@ -456,6 +457,8 @@ class InputBar extends StatelessWidget { builder: (context, controller, focusNode) => TextField( controller: controller, focusNode: focusNode, + contextMenuBuilder: (c, e) => + markdownContextBuilder(c, e, controller), contentInsertionConfiguration: ContentInsertionConfiguration( onContentInserted: (KeyboardInsertedContent content) { final data = content.data; diff --git a/lib/utils/markdown_context_builder.dart b/lib/utils/markdown_context_builder.dart new file mode 100644 index 000000000..bf33f948b --- /dev/null +++ b/lib/utils/markdown_context_builder.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +Widget markdownContextBuilder( + BuildContext context, + EditableTextState editableTextState, + TextEditingController controller, +) { + final value = editableTextState.textEditingValue; + final selectedText = value.selection.textInside(value.text); + final buttonItems = editableTextState.contextMenuButtonItems; + final l10n = L10n.of(context); + + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: editableTextState.contextMenuAnchors, + buttonItems: [ + ...buttonItems, + if (selectedText.isNotEmpty) ...[ + ContextMenuButtonItem( + label: l10n.link, + onPressed: () async { + final input = await showTextInputDialog( + context: context, + title: l10n.addLink, + okLabel: l10n.ok, + cancelLabel: l10n.cancel, + textFields: [ + DialogTextField( + validator: (text) { + if (text == null || text.isEmpty) { + return l10n.pleaseFillOut; + } + try { + text.startsWith('http') + ? Uri.parse(text) + : Uri.https(text); + } catch (_) { + return l10n.invalidUrl; + } + return null; + }, + hintText: 'www...', + keyboardType: TextInputType.url, + ), + ], + ); + final urlString = input?.singleOrNull; + if (urlString == null) return; + final url = urlString.startsWith('http') + ? Uri.parse(urlString) + : Uri.https(urlString); + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '[$selectedText](${url.toString()})', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.boldText, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '**$selectedText**', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.italicText, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '*$selectedText*', + ); + ContextMenuController.removeAny(); + }, + ), + ContextMenuButtonItem( + label: l10n.strikeThrough, + onPressed: () { + final selection = controller.selection; + controller.text = controller.text.replaceRange( + selection.start, + selection.end, + '~~$selectedText~~', + ); + ContextMenuController.removeAny(); + }, + ), + ], + ], + ); +}