From 2dae09408dfae8c76a398dbd97e940b579eadc48 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Wed, 13 Nov 2024 15:57:58 +0800 Subject: [PATCH] Allow pasting in amount input bottom sheet Fixes #157 --- assets/l10n/en_IN.json | 2 + assets/l10n/en_US.json | 2 + assets/l10n/it_IT.json | 2 + assets/l10n/mn_MN.json | 4 +- lib/routes/new_transaction/amount_text.dart | 20 +++- .../general/long_press_context_menu.dart | 98 +++++++++++++++++++ 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 lib/widgets/general/long_press_context_menu.dart diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index 44837e4..c8a9891 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -21,6 +21,7 @@ "general.delete.all": "Delete all", "general.copy.success": "Copied to clipboard", "general.copy.clickToCopy": "Click to copy", + "general.paste": "Paste", "general.timeSelector.select.month": "Select a month", "general.timeSelector.select.year": "Select a year", "general.timeSelector.now": "Now", @@ -319,6 +320,7 @@ "error.input.noImagePicked": "No image was selected", "error.input.cropFailed": "An error occured during cropping the picture", "error.input.wrongFileType": "Please choose a {type} file", + "error.input.pasteFormatMismatch": "Unable to parse", "error.sync.invalidBackupFile": "Invalid backup file", "error.sync.safetyBackupFailed": "Unable to start import", "error.sync.exportFailed": "Unable to export, please contact developer.", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index 58cb0b3..df95965 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -21,6 +21,7 @@ "general.delete.all": "Delete all", "general.copy.success": "Copied to clipboard", "general.copy.clickToCopy": "Click to copy", + "general.paste": "Paste", "general.timeSelector.select.month": "Select a month", "general.timeSelector.select.year": "Select a year", "general.timeSelector.now": "Now", @@ -319,6 +320,7 @@ "error.input.cropFailed": "An error occured during cropping the picture", "error.input.wrongFileType": "Please choose a {type} file", "error.sync.invalidBackupFile": "Invalid backup file", + "error.input.pasteFormatMismatch": "Unable to parse", "error.sync.safetyBackupFailed": "Unable to start import", "error.sync.exportFailed": "Unable to export, please contact developer.", "error.sync.fileDeleteFailed": "An error occured during backup deletion", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 24a9ba8..f6eee8f 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -21,6 +21,7 @@ "general.delete.all": "Elimina tutto", "general.copy.success": "Copiato negli appunti", "general.copy.clickToCopy": "Clicca per copiare", + "general.paste": "Incolla", "general.timeSelector.select.month": "Seleziona un mese", "general.timeSelector.select.year": "Seleziona un anno", "general.timeSelector.now": "Ora", @@ -319,6 +320,7 @@ "error.input.noImagePicked": "Nessuna immagine selezionata", "error.input.cropFailed": "Si è verificato un errore durante il ritaglio dell'immagine", "error.input.wrongFileType": "Si prega di scegliere un file {type}", + "error.input.pasteFormatMismatch": "Impossibile analizzare", "error.sync.invalidBackupFile": "File di backup non valido", "error.sync.safetyBackupFailed": "Impossibile avviare l'importazione", "error.sync.exportFailed": "Impossibile esportare, si prega di contattare lo sviluppatore.", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index cef89c4..5f7fa71 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -20,7 +20,8 @@ "general.select.all": "Бүгдийг сонгох", "general.delete.all": "Бүгдийг устгах", "general.copy.success": "Амжилттай хууллаа", - "general.copy.clickToCopy": "Дарвал бичвэр хуулна", + "general.copy.clickToCopy": "Дарвал бичвэрийг хуулна", + "general.paste": "Буулгах", "general.timeSelector.select.month": "Сар сонгох", "general.timeSelector.select.year": "Жил сонгох", "general.timeSelector.now": "Одоо", @@ -319,6 +320,7 @@ "error.input.noImagePicked": "Та зураг сонгоогүй байна", "error.input.cropFailed": "Зураг хайчлах үед алдаа гарлаа", "error.input.wrongFileType": "Зөвхөн {} төрлийн файл сонгох боломжтой", + "error.input.pasteFormatMismatch": "Өгөгдлийг таньж чадсангүй", "error.sync.invalidBackupFile": "Нөөц файл алдаатай байна", "error.sync.safetyBackupFailed": "Сэргээх үйлдэл эхлэх боломжгүй", "error.sync.exportFailed": "Нөөцлөх явцад алдаа гарлаа, хөгжүүлэгчид хандана уу.", diff --git a/lib/routes/new_transaction/amount_text.dart b/lib/routes/new_transaction/amount_text.dart index 5bd746f..150cf90 100644 --- a/lib/routes/new_transaction/amount_text.dart +++ b/lib/routes/new_transaction/amount_text.dart @@ -5,7 +5,9 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/routes/new_transaction/input_amount_sheet/input_value.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/utils.dart"; +import "package:flow/widgets/general/long_press_context_menu.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; class AmountText extends StatefulWidget { final FocusNode focusNode; @@ -90,8 +92,14 @@ class _AmountTextState extends State return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SelectionArea( - focusNode: widget.focusNode, + child: LongPressContextMenu( + actions: [ + PopupMenuItem( + value: "copy", + child: Text("general.copy".t(context)), + ), + ], + onSelected: handleContextMenuAction, child: Transform.scale( scale: _amountTextScaleAnimation.value, child: SizedBox( @@ -132,4 +140,12 @@ class _AmountTextState extends State await _amountTextAnimationController.forward().orCancel; await _amountTextAnimationController.reverse().orCancel; } + + void handleContextMenuAction(String? action) { + switch (action) { + case "copy": + Clipboard.setData(ClipboardData(text: amountText())); + break; + } + } } diff --git a/lib/widgets/general/long_press_context_menu.dart b/lib/widgets/general/long_press_context_menu.dart new file mode 100644 index 0000000..86e25f7 --- /dev/null +++ b/lib/widgets/general/long_press_context_menu.dart @@ -0,0 +1,98 @@ +import "package:flow/l10n/extensions.dart"; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; + +class LongPressContextMenu extends StatefulWidget { + final bool addPasteAction; + + final List> actions; + + final ValueChanged onSelected; + final void Function(String text)? onPaste; + + final Widget child; + + const LongPressContextMenu({ + super.key, + required this.child, + required this.actions, + required this.onSelected, + this.addPasteAction = false, + this.onPaste, + }); + + @override + State createState() => _LongPressContextMenuState(); +} + +class _LongPressContextMenuState extends State { + Offset _lastPointerPosition = Offset.zero; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: (PointerDownEvent event) { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) { + _lastPointerPosition = event.position; + + _open(); + } + }, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: (TapDownDetails details) { + _lastPointerPosition = details.globalPosition; + }, + onLongPress: _open, + child: widget.child, + ), + ); + } + + void _open() async { + final RenderBox? overlay = + Overlay.of(context).context.findRenderObject() as RenderBox?; + + if (overlay == null) return; + + String? pasteText; + + if (widget.addPasteAction && widget.onPaste != null) { + final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); + + final String? text = clipboardData?.text?.trim(); + + if (text != null && text.isNotEmpty) { + pasteText = text; + } + } + + if (!context.mounted) return; + + final String? value = await showMenu( + context: context, + items: [ + if (pasteText != null) + PopupMenuItem( + value: "paste", + child: Text("general.paste".t(context)), + ), + ...widget.actions + ], + position: RelativeRect.fromLTRB( + _lastPointerPosition.dx, + _lastPointerPosition.dy, + overlay.size.width - _lastPointerPosition.dx, + overlay.size.height - _lastPointerPosition.dy, + ), + ); + + if (value == "paste") { + widget.onPaste?.call(pasteText ?? ""); + } else { + widget.onSelected(value); + } + } +}