add context menu to messages

This commit is contained in:
otsmr 2025-06-26 00:20:54 +02:00
parent f64470b9dc
commit e0b3893d37
20 changed files with 479 additions and 228 deletions

View file

@ -1,5 +1,5 @@
{ {
"files.exclude": { "files.exclude": {
"dependencies": true "dependencies": false
} }
} }

@ -1 +1 @@
Subproject commit ba7804445efcf90ba98577cfecdf97a59d01e561 Subproject commit 22df3f2ab9ad71db60526668578a5309b3cc84ef

View file

@ -210,6 +210,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.go(); .go();
} }
Future deleteMessagesByMessageId(int messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
Future deleteAllMessagesByContactId(int contactId) { Future deleteAllMessagesByContactId(int contactId) {
return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
} }

View file

@ -156,6 +156,11 @@
"close": "Schließen", "close": "Schließen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"ok": "Ok", "ok": "Ok",
"react": "Reagieren",
"reply": "Antworten",
"copy": "Kopieren",
"delete": "Löschen",
"info": "Info",
"disable": "Deaktiviern", "disable": "Deaktiviern",
"enable": "Aktivieren", "enable": "Aktivieren",
"switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.", "switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.",
@ -260,6 +265,8 @@
"tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen", "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.", "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.", "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?", "deleteImageTitle": "Bist du dir sicher?",
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.",
"backupNoticeTitle": "Kein Backup konfiguriert", "backupNoticeTitle": "Kein Backup konfiguriert",

View file

@ -280,6 +280,11 @@
"disable": "Disable", "disable": "Disable",
"enable": "Enable", "enable": "Enable",
"cancel": "Cancel", "cancel": "Cancel",
"react": "React",
"reply": "Reply",
"copy": "Copy",
"delete": "Delete",
"info": "Info",
"ok": "Ok", "ok": "Ok",
"switchFrontAndBackCamera": "Switch between front and back camera.", "switchFrontAndBackCamera": "Switch between front and back camera.",
"@switchFrontAndBackCamera": {}, "@switchFrontAndBackCamera": {},
@ -413,6 +418,8 @@
"tutorialChatMessagesReopenMessageTitle": "Reopen Images and Videos", "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.", "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.", "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?", "deleteImageTitle": "Are you sure?",
"deleteImageBody": "The image will be irrevocably deleted.", "deleteImageBody": "The image will be irrevocably deleted.",
"settingsBackup": "Backup", "settingsBackup": "Backup",

View file

@ -944,6 +944,36 @@ abstract class AppLocalizations {
/// **'Cancel'** /// **'Cancel'**
String get 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. /// No description provided for @ok.
/// ///
/// In en, this message translates to: /// 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.'** /// **'As soon as you save pictures or videos, they end up here in your memories.'**
String get memoriesEmpty; 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. /// No description provided for @deleteImageTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -477,6 +477,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get cancel => 'Abbrechen'; 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 @override
String get ok => 'Ok'; String get ok => 'Ok';
@ -834,6 +849,12 @@ class AppLocalizationsDe extends AppLocalizations {
String get memoriesEmpty => String get memoriesEmpty =>
'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.'; '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 @override
String get deleteImageTitle => 'Bist du dir sicher?'; String get deleteImageTitle => 'Bist du dir sicher?';

View file

@ -472,6 +472,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get cancel => 'Cancel'; 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 @override
String get ok => 'Ok'; String get ok => 'Ok';
@ -828,6 +843,12 @@ class AppLocalizationsEn extends AppLocalizations {
String get memoriesEmpty => String get memoriesEmpty =>
'As soon as you save pictures or videos, they end up here in your memories.'; '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 @override
String get deleteImageTitle => 'Are you sure?'; String get deleteImageTitle => 'Are you sure?';

View file

@ -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/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart';
extension ShortCutsExtension on BuildContext { extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!; AppLocalizations get lang => AppLocalizations.of(this)!;
@ -413,3 +414,23 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
final double formattedSize = bytes / pow(1000, unitIndex); final double formattedSize = bytes / pow(1000, unitIndex);
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[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;
}
}

View file

@ -4,12 +4,14 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/globals.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.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/services/notifications/background.notifications.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.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/animate_icon.dart';
import 'package:twonly/src/views/components/initialsavatar.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/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
@ -285,7 +287,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
), ),
), ),
body: SafeArea( body: PieCanvas(
theme: getPieCanvasTheme(context),
child: SafeArea(
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
@ -303,8 +307,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
size = 99; size = 99;
} else { } else {
size = 11 + size = 11 +
calculateNumberOfLines(content.text, calculateNumberOfLines(
MediaQuery.of(context).size.width * 0.8, 17) * content.text,
MediaQuery.of(context).size.width * 0.8,
17) *
27; 27;
} }
} }
@ -407,7 +413,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
SizedBox(width: 8), SizedBox(width: 8),
(currentInputText != "") (currentInputText != "")
? IconButton( ? IconButton(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane), icon:
FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () {
_sendMessage(); _sendMessage();
}, },
@ -419,7 +426,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return CameraSendToView(widget.contact); return CameraSendToView(
widget.contact);
}, },
), ),
); );
@ -431,6 +439,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
], ],
), ),
), ),
),
); );
} }
} }
@ -458,6 +467,6 @@ double calculateNumberOfLines(String text, double width, double fontSize) {
), ),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
); );
textPainter.layout(maxWidth: (width - 30)); textPainter.layout(maxWidth: (width - 32));
return textPainter.computeLineMetrics().length.toDouble(); return textPainter.computeLineMetrics().length.toDouble();
} }

View file

@ -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_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_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/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/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
@ -66,14 +66,17 @@ class _ChatListEntryState extends State<ChatListEntry> {
crossAxisAlignment: crossAxisAlignment:
right ? CrossAxisAlignment.end : CrossAxisAlignment.start, right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
SlidingResponse( MessageActions(
message: widget.message,
child: Stack( child: Stack(
alignment: alignment:
right ? Alignment.centerRight : Alignment.centerLeft, right ? Alignment.centerRight : Alignment.centerLeft,
children: [ children: [
(textMessage != null) (textMessage != null)
? ChatTextEntry( ? ChatTextEntry(
message: widget.message, text: textMessage!) message: widget.message,
text: textMessage!,
)
: ChatMediaEntry( : ChatMediaEntry(
message: widget.message, message: widget.message,
contact: widget.contact, contact: widget.contact,

View file

@ -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<MessageActions> createState() => _SlidingResponseWidgetState();
}
class _SlidingResponseWidgetState extends State<MessageActions> {
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,
);
}
}

View file

@ -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<SlidingResponse> createState() => _SlidingResponseWidgetState();
}
class _SlidingResponseWidgetState extends State<SlidingResponse> {
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,
),
],
),
),
],
);
}
}

View file

@ -6,7 +6,7 @@ import 'package:twonly/src/utils/misc.dart';
Future<bool> showAlertDialog( Future<bool> showAlertDialog(
BuildContext context, BuildContext context,
String title, String title,
String content, { String? content, {
String? customOk, String? customOk,
String? customCancel, String? customCancel,
}) async { }) async {
@ -31,7 +31,7 @@ Future<bool> showAlertDialog(
// set up the AlertDialog // set up the AlertDialog
AlertDialog alert = AlertDialog( AlertDialog alert = AlertDialog(
title: Text(title), title: Text(title),
content: Text(content), content: (content == null) ? null : Text(content),
actions: [ actions: [
cancelButton, cancelButton,
okButton, okButton,

View file

@ -50,7 +50,7 @@ class BetterText extends StatelessWidget {
spans.add(TextSpan(text: text.substring(lastMatchEnd))); spans.add(TextSpan(text: text.substring(lastMatchEnd)));
} }
return SelectableText.rich( return Text.rich(
TextSpan( TextSpan(
children: spans, children: spans,
), ),

View file

@ -181,8 +181,7 @@ PieTheme getPieCanvasTheme(BuildContext context) {
iconColor: Theme.of(context).colorScheme.surfaceBright, iconColor: Theme.of(context).colorScheme.surfaceBright,
), ),
tooltipPadding: EdgeInsets.all(20), tooltipPadding: EdgeInsets.all(20),
overlayColor: const Color.fromARGB(41, 0, 0, 0), overlayColor: const Color.fromARGB(69, 0, 0, 0),
// spacing: 0, // spacing: 0,
tooltipTextStyle: TextStyle( tooltipTextStyle: TextStyle(
fontSize: 32, fontSize: 32,

View file

@ -222,6 +222,7 @@ class AdditionalUserInvite extends StatefulWidget {
class _AdditionalUserInviteState extends State<AdditionalUserInvite> { class _AdditionalUserInviteState extends State<AdditionalUserInvite> {
void _copyVoucherId() { void _copyVoucherId() {
Clipboard.setData(ClipboardData(text: widget.invite.inviteCode)); Clipboard.setData(ClipboardData(text: widget.invite.inviteCode));
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("${widget.invite.inviteCode} copied.")), SnackBar(content: Text("${widget.invite.inviteCode} copied.")),
); );

View file

@ -91,6 +91,7 @@ class _VoucherCardState extends State<VoucherCard> {
void _copyVoucherId() { void _copyVoucherId() {
if (!widget.voucher.redeemed) { if (!widget.voucher.redeemed) {
Clipboard.setData(ClipboardData(text: widget.voucher.voucherId)); Clipboard.setData(ClipboardData(text: widget.voucher.voucherId));
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("${widget.voucher.voucherId} copied.")), SnackBar(content: Text("${widget.voucher.voucherId} copied.")),
); );

View file

@ -1286,10 +1286,9 @@ packages:
pie_menu: pie_menu:
dependency: "direct main" dependency: "direct main"
description: description:
name: pie_menu path: "dependencies/flutter-pie-menu"
sha256: "47e29f43fbe896ec47513b155762a661f6cd243317cd8e91ed5303ad0b9f6141" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "3.3.0" version: "3.3.0"
platform: platform:
dependency: transitive dependency: transitive

View file

@ -27,6 +27,9 @@ dependencies:
path: ./dependencies/flutter_secure_storage/flutter_secure_storage path: ./dependencies/flutter_secure_storage/flutter_secure_storage
flutter_zxing: flutter_zxing:
path: ./dependencies/flutter_zxing path: ./dependencies/flutter_zxing
# pie_menu: ^3.2.7
pie_menu:
path: ./dependencies/flutter-pie-menu
font_awesome_flutter: ^10.8.0 font_awesome_flutter: ^10.8.0
gal: ^2.3.1 gal: ^2.3.1
hand_signature: ^3.0.3 hand_signature: ^3.0.3
@ -43,7 +46,6 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.5 path_provider: ^2.1.5
permission_handler: ^12.0.0+1 permission_handler: ^12.0.0+1
pie_menu: ^3.2.7
protobuf: ^4.0.0 protobuf: ^4.0.0
cryptography_plus: ^2.7.0 cryptography_plus: ^2.7.0
provider: ^6.1.2 provider: ^6.1.2