implemented a central haptic feedback system with configurable levels (off, reduced, full), added a Haptics facade providing semantic feedback methods, integrated haptic cues across navigation, settings toggles, and async action results, and updated version to 1.1.0+54

This commit is contained in:
2026-05-30 13:54:19 +02:00
parent 01b4b44010
commit ece0669f7d
26 changed files with 308 additions and 75 deletions
+19 -7
View File
@@ -9,6 +9,7 @@ import '../../../../routing/app_routes.dart';
import '../../../../share_intent/remote_file_ref.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../utils/download_manager.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/info_dialog.dart';
import '../data/chat_bubble_styles.dart';
@@ -63,6 +64,7 @@ class _ChatBubbleState extends State<ChatBubble>
Offset _position = Offset.zero;
Offset _dragStartPosition = Offset.zero;
bool _swipeActionArmed = false;
@override
void initState() {
@@ -97,6 +99,7 @@ class _ChatBubbleState extends State<ChatBubble>
if (job == null) return;
final status = job.status.value;
if (status is DownloadDone) {
Haptics.success();
DownloadManager.instance.clear(job.remotePath);
_detachJob();
final talkFile = message.file;
@@ -197,13 +200,16 @@ class _ChatBubbleState extends State<ChatBubble>
}
}
void _showOptionsDialog() => showChatMessageOptionsDialog(
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
void _showOptionsDialog() {
Haptics.longPress();
showChatMessageOptionsDialog(
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
}
/// True only for messages whose body has a meaningful tap action (poll
/// dialog or file download/cancel). For plain text messages we leave
@@ -302,10 +308,16 @@ class _ChatBubbleState extends State<ChatBubble>
? Offset(_position.dx, 0)
: Offset(_position.dx + dx, 0);
});
// Beim Überqueren der Action-Schwelle einmalig Haptik feuern,
// damit der User physisch spürt: "jetzt löst's beim Loslassen aus".
final isArmed = _position.dx.abs() > 50;
if (isArmed && !_swipeActionArmed) Haptics.longPress();
_swipeActionArmed = isArmed;
},
onHorizontalDragEnd: (_) {
final isAction = _position.dx.abs() > 50;
setState(() => _position = Offset.zero);
_swipeActionArmed = false;
if (widget.bubbleData.isReplyable && isAction) {
context.read<ChatBloc>().setReferenceMessageId(
widget.bubbleData.id,
@@ -13,6 +13,7 @@ import '../../../../notification/notification_tasks.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
@@ -166,6 +167,7 @@ class _ChatTileState extends State<ChatTile> {
},
onLongPress: () {
if (widget.disableContextActions) return;
Haptics.longPress();
showDetailsBottomSheet(
context,
children: (sheetCtx) => [