From e0b3893d37bec4230e24881549ea3cace5a5bcf0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 26 Jun 2025 00:20:54 +0200 Subject: [PATCH] add context menu to messages --- .vscode/settings.json | 2 +- dependencies/flutter-pie-menu | 2 +- lib/src/database/daos/messages_dao.dart | 4 + lib/src/localization/app_de.arb | 7 + lib/src/localization/app_en.arb | 7 + .../generated/app_localizations.dart | 42 +++ .../generated/app_localizations_de.dart | 21 ++ .../generated/app_localizations_en.dart | 21 ++ lib/src/utils/misc.dart | 21 ++ lib/src/views/chats/chat_messages.view.dart | 275 +++++++++--------- .../chat_message_entry.dart | 9 +- .../message_actions.dart | 194 ++++++++++++ .../sliding_response.dart | 80 ----- lib/src/views/components/alert_dialog.dart | 4 +- lib/src/views/components/better_text.dart | 2 +- .../views/components/user_context_menu.dart | 3 +- .../subscription/additional_users.view.dart | 1 + .../settings/subscription/voucher.view.dart | 1 + pubspec.lock | 7 +- pubspec.yaml | 4 +- 20 files changed, 479 insertions(+), 228 deletions(-) create mode 100644 lib/src/views/chats/chat_messages_components/message_actions.dart delete mode 100644 lib/src/views/chats/chat_messages_components/sliding_response.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index b2158a2..41c6f8e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "files.exclude": { - "dependencies": true + "dependencies": false } } \ No newline at end of file diff --git a/dependencies/flutter-pie-menu b/dependencies/flutter-pie-menu index ba78044..22df3f2 160000 --- a/dependencies/flutter-pie-menu +++ b/dependencies/flutter-pie-menu @@ -1 +1 @@ -Subproject commit ba7804445efcf90ba98577cfecdf97a59d01e561 +Subproject commit 22df3f2ab9ad71db60526668578a5309b3cc84ef diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index 4e916e4..813ce5b 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -210,6 +210,10 @@ class MessagesDao extends DatabaseAccessor .go(); } + Future deleteMessagesByMessageId(int messageId) { + return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); + } + Future deleteAllMessagesByContactId(int contactId) { return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index e1022c0..4d7aa71 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -156,6 +156,11 @@ "close": "Schließen", "cancel": "Abbrechen", "ok": "Ok", + "react": "Reagieren", + "reply": "Antworten", + "copy": "Kopieren", + "delete": "Löschen", + "info": "Info", "disable": "Deaktiviern", "enable": "Aktivieren", "switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.", @@ -260,6 +265,8 @@ "tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen", "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", + "deleteTitle": "Bist du dir sicher?", + "deleteOkBtn": "Für mich löschen", "deleteImageTitle": "Bist du dir sicher?", "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", "backupNoticeTitle": "Kein Backup konfiguriert", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 5f78766..0081d7b 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -280,6 +280,11 @@ "disable": "Disable", "enable": "Enable", "cancel": "Cancel", + "react": "React", + "reply": "Reply", + "copy": "Copy", + "delete": "Delete", + "info": "Info", "ok": "Ok", "switchFrontAndBackCamera": "Switch between front and back camera.", "@switchFrontAndBackCamera": {}, @@ -413,6 +418,8 @@ "tutorialChatMessagesReopenMessageTitle": "Reopen Images and Videos", "tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.", "memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.", + "deleteTitle": "Are you sure?", + "deleteOkBtn": "Delete for me", "deleteImageTitle": "Are you sure?", "deleteImageBody": "The image will be irrevocably deleted.", "settingsBackup": "Backup", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 13ea3a5..6d39214 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -944,6 +944,36 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// No description provided for @react. + /// + /// In en, this message translates to: + /// **'React'** + String get react; + + /// No description provided for @reply. + /// + /// In en, this message translates to: + /// **'Reply'** + String get reply; + + /// No description provided for @copy. + /// + /// In en, this message translates to: + /// **'Copy'** + String get copy; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @info. + /// + /// In en, this message translates to: + /// **'Info'** + String get info; + /// No description provided for @ok. /// /// In en, this message translates to: @@ -1574,6 +1604,18 @@ abstract class AppLocalizations { /// **'As soon as you save pictures or videos, they end up here in your memories.'** String get memoriesEmpty; + /// No description provided for @deleteTitle. + /// + /// In en, this message translates to: + /// **'Are you sure?'** + String get deleteTitle; + + /// No description provided for @deleteOkBtn. + /// + /// In en, this message translates to: + /// **'Delete for me'** + String get deleteOkBtn; + /// No description provided for @deleteImageTitle. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 6e1f77c..6730920 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -477,6 +477,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get react => 'Reagieren'; + + @override + String get reply => 'Antworten'; + + @override + String get copy => 'Kopieren'; + + @override + String get delete => 'Löschen'; + + @override + String get info => 'Info'; + @override String get ok => 'Ok'; @@ -834,6 +849,12 @@ class AppLocalizationsDe extends AppLocalizations { String get memoriesEmpty => 'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.'; + @override + String get deleteTitle => 'Bist du dir sicher?'; + + @override + String get deleteOkBtn => 'Für mich löschen'; + @override String get deleteImageTitle => 'Bist du dir sicher?'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 9937b15..cb2639f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -472,6 +472,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get react => 'React'; + + @override + String get reply => 'Reply'; + + @override + String get copy => 'Copy'; + + @override + String get delete => 'Delete'; + + @override + String get info => 'Info'; + @override String get ok => 'Ok'; @@ -828,6 +843,12 @@ class AppLocalizationsEn extends AppLocalizations { String get memoriesEmpty => 'As soon as you save pictures or videos, they end up here in your memories.'; + @override + String get deleteTitle => 'Are you sure?'; + + @override + String get deleteOkBtn => 'Delete for me'; + @override String get deleteImageTitle => 'Are you sure?'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 6522fa3..e299b95 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/utils/log.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; @@ -413,3 +414,23 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { final double formattedSize = bytes / pow(1000, unitIndex); return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}"; } + +String getMessageText(Message message) { + try { + if (message.contentJson == null) return ""; + return TextMessageContent.fromJson(jsonDecode(message.contentJson!)).text; + } catch (e) { + Log.error(e); + return ""; + } +} + +MediaMessageContent? getMediaContent(Message message) { + try { + if (message.contentJson == null) return null; + return MediaMessageContent.fromJson(jsonDecode(message.contentJson!)); + } catch (e) { + Log.error(e); + return null; + } +} diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 8153c53..be5eaa3 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -4,12 +4,14 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; +import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/twonly_database.dart'; @@ -285,150 +287,157 @@ class _ChatMessagesViewState extends State { ), ), ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: messages.length + 1, - reverse: true, - itemExtentBuilder: (index, dimensions) { - if (index == 0) return 10; // empty padding - index -= 1; - double size = 44; - if (messages[index].kind == MessageKind.textMessage) { - TextMessageContent? content = TextMessageContent.fromJson( - jsonDecode(messages[index].contentJson!)); - if (EmojiAnimation.supported(content.text)) { - size = 99; - } else { - size = 11 + - calculateNumberOfLines(content.text, - MediaQuery.of(context).size.width * 0.8, 17) * - 27; - } - } - if (messages[index].mediaStored) { - size = 271; - } - final reactions = - textReactionsToMessageId[messages[index].messageId]; - if (reactions != null && reactions.isNotEmpty) { - for (final reaction in reactions) { - if (reaction.kind == MessageKind.textMessage) { - TextMessageContent? content = - TextMessageContent.fromJson( - jsonDecode(reaction.contentJson!)); - size += calculateNumberOfLines(content.text, - MediaQuery.of(context).size.width * 0.5, 14) * - 27; + body: PieCanvas( + theme: getPieCanvasTheme(context), + child: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: messages.length + 1, + reverse: true, + itemExtentBuilder: (index, dimensions) { + if (index == 0) return 10; // empty padding + index -= 1; + double size = 44; + if (messages[index].kind == MessageKind.textMessage) { + TextMessageContent? content = TextMessageContent.fromJson( + jsonDecode(messages[index].contentJson!)); + if (EmojiAnimation.supported(content.text)) { + size = 99; + } else { + size = 11 + + calculateNumberOfLines( + content.text, + MediaQuery.of(context).size.width * 0.8, + 17) * + 27; + } + } + if (messages[index].mediaStored) { + size = 271; + } + final reactions = + textReactionsToMessageId[messages[index].messageId]; + if (reactions != null && reactions.isNotEmpty) { + for (final reaction in reactions) { + if (reaction.kind == MessageKind.textMessage) { + TextMessageContent? content = + TextMessageContent.fromJson( + jsonDecode(reaction.contentJson!)); + size += calculateNumberOfLines(content.text, + MediaQuery.of(context).size.width * 0.5, 14) * + 27; + } } } - } - if (!isLastMessageFromSameUser(messages, index)) { - size += 20; - } - return size; - }, - itemBuilder: (context, i) { - if (i == 0) { - return Container(); // just a padding - } - i -= 1; - return ChatListEntry( - key: Key(messages[i].messageId.toString()), - messages[i], - user, - galleryItems, - isLastMessageFromSameUser(messages, i), - textReactionsToMessageId[messages[i].messageId] ?? [], - emojiReactionsToMessageId[messages[i].messageId] ?? [], - onResponseTriggered: (message) { - setState(() { - responseToMessage = message; - }); - textFieldFocus.requestFocus(); - }, - ); - }, + if (!isLastMessageFromSameUser(messages, index)) { + size += 20; + } + return size; + }, + itemBuilder: (context, i) { + if (i == 0) { + return Container(); // just a padding + } + i -= 1; + return ChatListEntry( + key: Key(messages[i].messageId.toString()), + messages[i], + user, + galleryItems, + isLastMessageFromSameUser(messages, i), + textReactionsToMessageId[messages[i].messageId] ?? [], + emojiReactionsToMessageId[messages[i].messageId] ?? [], + onResponseTriggered: (message) { + setState(() { + responseToMessage = message; + }); + textFieldFocus.requestFocus(); + }, + ); + }, + ), ), - ), - if (responseToMessage != null && !user.deleted) - Container( + if (responseToMessage != null && !user.deleted) + Container( + padding: const EdgeInsets.only( + bottom: 00, + left: 20, + right: 20, + top: 10, + ), + child: Row( + children: [ + Expanded(child: getResponsePreview(responseToMessage!)), + IconButton( + onPressed: () { + setState(() { + responseToMessage = null; + }); + }, + icon: FaIcon( + FontAwesomeIcons.xmark, + size: 16, + ), + ) + ], + ), + ), + Padding( padding: const EdgeInsets.only( - bottom: 00, + bottom: 30, left: 20, right: 20, top: 10, ), child: Row( - children: [ - Expanded(child: getResponsePreview(responseToMessage!)), - IconButton( - onPressed: () { - setState(() { - responseToMessage = null; - }); - }, - icon: FaIcon( - FontAwesomeIcons.xmark, - size: 16, - ), - ) - ], + children: (user.deleted) + ? [] + : [ + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: inputTextMessageDeco(context), + ), + ), + SizedBox(width: 8), + (currentInputText != "") + ? IconButton( + icon: + FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () { + _sendMessage(); + }, + ) + : IconButton( + icon: FaIcon(FontAwesomeIcons.camera), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView( + widget.contact); + }, + ), + ); + }, + ) + ], ), ), - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: (user.deleted) - ? [] - : [ - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), - ), - ), - SizedBox(width: 8), - (currentInputText != "") - ? IconButton( - icon: FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () { - _sendMessage(); - }, - ) - : IconButton( - icon: FaIcon(FontAwesomeIcons.camera), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.contact); - }, - ), - ); - }, - ) - ], - ), - ), - ], + ], + ), ), ), ); @@ -458,6 +467,6 @@ double calculateNumberOfLines(String text, double width, double fontSize) { ), textDirection: TextDirection.ltr, ); - textPainter.layout(maxWidth: (width - 30)); + textPainter.layout(maxWidth: (width - 32)); return textPainter.computeLineMetrics().length.toDouble(); } diff --git a/lib/src/views/chats/chat_messages_components/chat_message_entry.dart b/lib/src/views/chats/chat_messages_components/chat_message_entry.dart index c301623..91d22d6 100644 --- a/lib/src/views/chats/chat_messages_components/chat_message_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_message_entry.dart @@ -4,7 +4,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_text_response_columns.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/memory_item.model.dart'; @@ -66,14 +66,17 @@ class _ChatListEntryState extends State { crossAxisAlignment: right ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - SlidingResponse( + MessageActions( + message: widget.message, child: Stack( alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ (textMessage != null) ? ChatTextEntry( - message: widget.message, text: textMessage!) + message: widget.message, + text: textMessage!, + ) : ChatMediaEntry( message: widget.message, contact: widget.contact, diff --git a/lib/src/views/chats/chat_messages_components/message_actions.dart b/lib/src/views/chats/chat_messages_components/message_actions.dart new file mode 100644 index 0000000..8390c2e --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/message_actions.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pie_menu/pie_menu.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; + +class MessageActions extends StatefulWidget { + final Widget child; + final Message message; + final VoidCallback onResponseTriggered; + + const MessageActions({ + super.key, + required this.child, + required this.message, + required this.onResponseTriggered, + }); + + @override + State createState() => _SlidingResponseWidgetState(); +} + +class _SlidingResponseWidgetState extends State { + double _offsetX = 0.0; + bool gotFeedback = false; + + void _onHorizontalDragUpdate(DragUpdateDetails details) { + setState(() { + _offsetX += details.delta.dx; + if (_offsetX > 40) { + _offsetX = 40; + if (!gotFeedback) { + HapticFeedback.heavyImpact(); + gotFeedback = true; + } + } + if (_offsetX < 30) { + gotFeedback = false; + } + if (_offsetX <= 0) _offsetX = 0; + }); + } + + void _onHorizontalDragEnd(DragEndDetails details) { + if (_offsetX >= 40) { + widget.onResponseTriggered(); + } + setState(() { + _offsetX = 0.0; + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Transform.translate( + offset: Offset(_offsetX, 0), + child: GestureDetector( + onHorizontalDragUpdate: _onHorizontalDragUpdate, + onHorizontalDragEnd: _onHorizontalDragEnd, + child: MessageContextMenu( + message: widget.message, + onResponseTriggered: widget.onResponseTriggered, + child: widget.child, + ), + ), + ), + if (_offsetX >= 40) + Positioned( + left: 20, + top: 0, + bottom: 0, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.reply, + size: 14, + // color: Colors.green, + ), + ], + ), + ), + ], + ); + } +} + +class MessageContextMenu extends StatelessWidget { + final Widget child; + final Message message; + final VoidCallback onResponseTriggered; + + const MessageContextMenu({ + super.key, + required this.message, + required this.child, + required this.onResponseTriggered, + }); + + @override + Widget build(BuildContext context) { + return PieMenu( + onPressed: () => (), + onToggle: (menuOpen) { + if (menuOpen) { + HapticFeedback.heavyImpact(); + } + }, + actions: [ + PieAction( + tooltip: Text(context.lang.react), + onSelect: () async { + EmojiLayerData? layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const Emojis(); + }, + ); + if (layer == null) return; + Log.info(layer.text); + + sendTextMessage( + message.contactId, + TextMessageContent( + text: layer.text, + responseToMessageId: message.messageOtherId, + responseToOtherMessageId: (message.messageOtherId == null) + ? message.messageId + : null), + PushNotification( + kind: (message.kind == MessageKind.textMessage) + ? PushKind.reactionToText + : (getMediaContent(message)!.isVideo) + ? PushKind.reactionToVideo + : PushKind.reactionToText, + reactionContent: layer.text, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.faceLaugh), + ), + PieAction( + tooltip: Text(context.lang.reply), + onSelect: onResponseTriggered, + child: const FaIcon(FontAwesomeIcons.reply), + ), + PieAction( + tooltip: Text(context.lang.copy), + onSelect: () { + final text = getMessageText(message); + Clipboard.setData(ClipboardData(text: text)); + HapticFeedback.heavyImpact(); + }, + child: const FaIcon(FontAwesomeIcons.solidCopy), + ), + PieAction( + tooltip: Text(context.lang.delete), + onSelect: () async { + bool delete = await showAlertDialog( + context, + context.lang.deleteTitle, + null, + customOk: context.lang.deleteOkBtn, + ); + if (delete) { + await twonlyDB.messagesDao + .deleteMessagesByMessageId(message.messageId); + } + }, + child: const FaIcon(FontAwesomeIcons.trash), + ), + // PieAction( + // tooltip: Text(context.lang.info), + // onSelect: () {}, + // child: const FaIcon(FontAwesomeIcons.circleInfo), + // ), + ], + child: child, + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/sliding_response.dart b/lib/src/views/chats/chat_messages_components/sliding_response.dart deleted file mode 100644 index 1add935..0000000 --- a/lib/src/views/chats/chat_messages_components/sliding_response.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - -class SlidingResponse extends StatefulWidget { - final Widget child; - final VoidCallback onResponseTriggered; - - const SlidingResponse({ - super.key, - required this.child, - required this.onResponseTriggered, - }); - - @override - State createState() => _SlidingResponseWidgetState(); -} - -class _SlidingResponseWidgetState extends State { - double _offset = 0.0; - bool gotFeedback = false; - - void _onHorizontalDragUpdate(DragUpdateDetails details) { - setState(() { - _offset += details.delta.dx; - if (_offset > 40) { - _offset = 40; - if (!gotFeedback) { - HapticFeedback.heavyImpact(); - gotFeedback = true; - } - } - if (_offset < 30) { - gotFeedback = false; - } - if (_offset <= 0) _offset = 0; - }); - } - - void _onHorizontalDragEnd(DragEndDetails details) { - if (_offset >= 40) { - widget.onResponseTriggered(); - } - setState(() { - _offset = 0.0; - }); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Transform.translate( - offset: Offset(_offset, 0), - child: GestureDetector( - onHorizontalDragUpdate: _onHorizontalDragUpdate, - onHorizontalDragEnd: _onHorizontalDragEnd, - child: widget.child, - ), - ), - if (_offset >= 40) - Positioned( - left: 20, - top: 0, - bottom: 0, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FaIcon( - FontAwesomeIcons.reply, - size: 14, - // color: Colors.green, - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/src/views/components/alert_dialog.dart b/lib/src/views/components/alert_dialog.dart index 8db0932..d676f31 100644 --- a/lib/src/views/components/alert_dialog.dart +++ b/lib/src/views/components/alert_dialog.dart @@ -6,7 +6,7 @@ import 'package:twonly/src/utils/misc.dart'; Future showAlertDialog( BuildContext context, String title, - String content, { + String? content, { String? customOk, String? customCancel, }) async { @@ -31,7 +31,7 @@ Future showAlertDialog( // set up the AlertDialog AlertDialog alert = AlertDialog( title: Text(title), - content: Text(content), + content: (content == null) ? null : Text(content), actions: [ cancelButton, okButton, diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index defa113..53691fa 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -50,7 +50,7 @@ class BetterText extends StatelessWidget { spans.add(TextSpan(text: text.substring(lastMatchEnd))); } - return SelectableText.rich( + return Text.rich( TextSpan( children: spans, ), diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart index b9ee952..01f5205 100644 --- a/lib/src/views/components/user_context_menu.dart +++ b/lib/src/views/components/user_context_menu.dart @@ -181,8 +181,7 @@ PieTheme getPieCanvasTheme(BuildContext context) { iconColor: Theme.of(context).colorScheme.surfaceBright, ), tooltipPadding: EdgeInsets.all(20), - overlayColor: const Color.fromARGB(41, 0, 0, 0), - + overlayColor: const Color.fromARGB(69, 0, 0, 0), // spacing: 0, tooltipTextStyle: TextStyle( fontSize: 32, diff --git a/lib/src/views/settings/subscription/additional_users.view.dart b/lib/src/views/settings/subscription/additional_users.view.dart index 109a11c..ab92723 100644 --- a/lib/src/views/settings/subscription/additional_users.view.dart +++ b/lib/src/views/settings/subscription/additional_users.view.dart @@ -222,6 +222,7 @@ class AdditionalUserInvite extends StatefulWidget { class _AdditionalUserInviteState extends State { void _copyVoucherId() { Clipboard.setData(ClipboardData(text: widget.invite.inviteCode)); + HapticFeedback.heavyImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("${widget.invite.inviteCode} copied.")), ); diff --git a/lib/src/views/settings/subscription/voucher.view.dart b/lib/src/views/settings/subscription/voucher.view.dart index e73aed6..b8cca24 100644 --- a/lib/src/views/settings/subscription/voucher.view.dart +++ b/lib/src/views/settings/subscription/voucher.view.dart @@ -91,6 +91,7 @@ class _VoucherCardState extends State { void _copyVoucherId() { if (!widget.voucher.redeemed) { Clipboard.setData(ClipboardData(text: widget.voucher.voucherId)); + HapticFeedback.heavyImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("${widget.voucher.voucherId} copied.")), ); diff --git a/pubspec.lock b/pubspec.lock index d38fe62..605b79e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1286,10 +1286,9 @@ packages: pie_menu: dependency: "direct main" description: - name: pie_menu - sha256: "47e29f43fbe896ec47513b155762a661f6cd243317cd8e91ed5303ad0b9f6141" - url: "https://pub.dev" - source: hosted + path: "dependencies/flutter-pie-menu" + relative: true + source: path version: "3.3.0" platform: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 462563c..1c261ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,9 @@ dependencies: path: ./dependencies/flutter_secure_storage/flutter_secure_storage flutter_zxing: path: ./dependencies/flutter_zxing + # pie_menu: ^3.2.7 + pie_menu: + path: ./dependencies/flutter-pie-menu font_awesome_flutter: ^10.8.0 gal: ^2.3.1 hand_signature: ^3.0.3 @@ -43,7 +46,6 @@ dependencies: path: ^1.9.0 path_provider: ^2.1.5 permission_handler: ^12.0.0+1 - pie_menu: ^3.2.7 protobuf: ^4.0.0 cryptography_plus: ^2.7.0 provider: ^6.1.2