fix #323
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2025-12-07 00:51:43 +01:00
parent 35a90ace0a
commit a63343ccdd
8 changed files with 238 additions and 97 deletions

View file

@ -829,5 +829,7 @@
"inProcess": "Wird verarbeitet", "inProcess": "Wird verarbeitet",
"draftMessage": "Entwurf", "draftMessage": "Entwurf",
"exportMemories": "Memories exportieren (Beta)", "exportMemories": "Memories exportieren (Beta)",
"importMemories": "Memories importieren (Beta)" "importMemories": "Memories importieren (Beta)",
"voiceMessageSlideToCancel": "Zum Abbrechen ziehen",
"voiceMessageCancel": "Abbrechen"
} }

View file

@ -607,5 +607,7 @@
"inProcess": "In process", "inProcess": "In process",
"draftMessage": "Draft", "draftMessage": "Draft",
"exportMemories": "Export memories (Beta)", "exportMemories": "Export memories (Beta)",
"importMemories": "Import memories (Beta)" "importMemories": "Import memories (Beta)",
"voiceMessageSlideToCancel": "Slide to cancel",
"voiceMessageCancel": "Cancel"
} }

View file

@ -2749,6 +2749,18 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Import memories (Beta)'** /// **'Import memories (Beta)'**
String get importMemories; String get importMemories;
/// No description provided for @voiceMessageSlideToCancel.
///
/// In en, this message translates to:
/// **'Slide to cancel'**
String get voiceMessageSlideToCancel;
/// No description provided for @voiceMessageCancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get voiceMessageCancel;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1516,4 +1516,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get importMemories => 'Memories importieren (Beta)'; String get importMemories => 'Memories importieren (Beta)';
@override
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
@override
String get voiceMessageCancel => 'Abbrechen';
} }

View file

@ -1506,4 +1506,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get importMemories => 'Import memories (Beta)'; String get importMemories => 'Import memories (Beta)';
@override
String get voiceMessageSlideToCancel => 'Slide to cancel';
@override
String get voiceMessageCancel => 'Cancel';
} }

View file

@ -84,11 +84,6 @@ class ChatAudioEntry extends StatelessWidget {
path: mediaService.tempPath.path, path: mediaService.tempPath.path,
message: message, message: message,
) )
: (mediaService.originalPath.existsSync())
? InChatAudioPlayer(
path: mediaService.originalPath.path,
message: message,
)
: Container() : Container()
else else
MessageSendStateIcon([message], [mediaService.mediaFile]), MessageSendStateIcon([message], [mediaService.mediaFile]),
@ -138,14 +133,22 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
} }
}); });
_playerController.onCurrentDurationChanged.listen((duration) { _playerController.onPlayerStateChanged.listen((a) async {
if (a == PlayerState.initialized) {
_displayDuration =
await _playerController.getDuration(DurationType.max);
_maxDuration = _displayDuration;
setState(() {});
}
});
_playerController.onCurrentDurationChanged.listen((duration) async {
if (mounted) { if (mounted) {
setState(() { setState(() {
_displayDuration = _maxDuration - duration; _displayDuration = _maxDuration - duration;
}); });
} }
}); });
initAsync();
} }
@override @override
@ -154,13 +157,6 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
super.dispose(); super.dispose();
} }
Future<void> initAsync() async {
_displayDuration = await _playerController.getDuration(DurationType.max);
_maxDuration = _displayDuration;
if (!mounted) return;
setState(() {});
}
bool _isPlaying = false; bool _isPlaying = false;
@override @override

View file

@ -15,6 +15,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
class MessageInput extends StatefulWidget { class MessageInput extends StatefulWidget {
const MessageInput({ const MessageInput({
@ -41,6 +42,10 @@ class _MessageInputState extends State<MessageInput> {
late final RecorderController recorderController; late final RecorderController recorderController;
final bool isApple = Platform.isIOS; final bool isApple = Platform.isIOS;
bool _emojiShowing = false; bool _emojiShowing = false;
bool _audioRecordingLock = false;
int _currentDuration = 0;
double _cancelSlideOffset = 0;
Offset _recordingOffset = Offset.zero;
RecordingState _recordingState = RecordingState.none; RecordingState _recordingState = RecordingState.none;
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
@ -72,11 +77,17 @@ class _MessageInputState extends State<MessageInput> {
void dispose() { void dispose() {
widget.textFieldFocus.removeListener(_handleTextFocusChange); widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose(); widget.textFieldFocus.dispose();
recorderController.dispose();
super.dispose(); super.dispose();
} }
void _initializeControllers() { void _initializeControllers() {
recorderController = RecorderController(); recorderController = RecorderController();
recorderController.onCurrentDuration.listen((duration) {
setState(() {
_currentDuration = duration.inMilliseconds;
});
});
} }
void _handleTextFocusChange() { void _handleTextFocusChange() {
@ -87,9 +98,37 @@ class _MessageInputState extends State<MessageInput> {
} }
} }
Future<void> _startAudioRecording() async {
if (!await Permission.microphone.isGranted) {
final statuses = await [
Permission.microphone,
].request();
if (statuses[Permission.microphone]!.isPermanentlyDenied) {
await openAppSettings();
return;
}
if (!await Permission.microphone.isGranted) {
return;
}
}
setState(() {
_recordingState = RecordingState.recording;
});
await HapticFeedback.heavyImpact();
final audioTmpPath =
'${(await getApplicationCacheDirectory()).path}/recording.m4a';
unawaited(
recorderController.record(
path: audioTmpPath,
),
);
}
Future<void> _stopAudioRecording() async { Future<void> _stopAudioRecording() async {
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
setState(() { setState(() {
_audioRecordingLock = false;
_cancelSlideOffset = 0;
_recordingState = RecordingState.none; _recordingState = RecordingState.none;
}); });
@ -114,6 +153,19 @@ class _MessageInputState extends State<MessageInput> {
); );
} }
Future<void> _cancelAudioRecording() async {
setState(() {
_audioRecordingLock = false;
_cancelSlideOffset = 0;
_recordingState = RecordingState.none;
});
final path = await recorderController.stop();
if (path == null) return;
if (File(path).existsSync()) {
File(path).deleteSync();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -169,27 +221,53 @@ class _MessageInputState extends State<MessageInput> {
), ),
Expanded( Expanded(
child: (_recordingState == RecordingState.recording) child: (_recordingState == RecordingState.recording)
? AudioWaveforms( ? Row(
enableGesture: true, children: [
size: Size( const Padding(
MediaQuery.of(context).size.width / 2, padding: EdgeInsets.only(
50, top: 14,
bottom: 14,
left: 12,
right: 8,
), ),
recorderController: recorderController, child: FaIcon(
waveStyle: WaveStyle( FontAwesomeIcons.microphone,
waveColor: isDarkMode(context) size: 20,
? Colors.white color: Colors.red,
: Colors.black,
extendWaveform: true,
showMiddleLine: false,
), ),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: context.color.surfaceContainer,
), ),
padding: const EdgeInsets.only(left: 18), const SizedBox(width: 10),
margin: Text(
const EdgeInsets.symmetric(horizontal: 15), formatMsToMinSec(
_currentDuration,
),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
if (!_audioRecordingLock) ...[
SizedBox(
width: (100 - _cancelSlideOffset) % 101,
),
Text(
context.lang.voiceMessageSlideToCancel),
] else ...[
Expanded(
child: Container(),
),
GestureDetector(
onTap: _cancelAudioRecording,
child: Text(
context.lang.voiceMessageCancel,
style: const TextStyle(
color: Colors.red,
),
),
),
const SizedBox(width: 20)
],
],
) )
: TextField( : TextField(
controller: _textFieldController, controller: _textFieldController,
@ -220,47 +298,84 @@ class _MessageInputState extends State<MessageInput> {
), ),
if (_textFieldController.text == '') if (_textFieldController.text == '')
GestureDetector( GestureDetector(
onLongPressStart: (a) async { onLongPressMoveUpdate: (details) {
if (!await Permission.microphone.isGranted) { if (_audioRecordingLock) return;
final statuses = await [ if (_recordingOffset.dy -
Permission.microphone, details.localPosition.dy >=
].request(); 100) {
if (statuses[Permission.microphone]! HapticFeedback.heavyImpact();
.isPermanentlyDenied) {
await openAppSettings();
return;
}
if (!await Permission.microphone.isGranted) {
return;
}
}
setState(() { setState(() {
_recordingState = RecordingState.recording; _audioRecordingLock = true;
}); });
await HapticFeedback.heavyImpact();
final audioTmpPath =
'${(await getApplicationCacheDirectory()).path}/recording.m4a';
unawaited(
recorderController.record(
path: audioTmpPath,
),
);
},
onLongPressCancel: () async {
final path = await recorderController.stop();
if (path == null) return;
if (File(path).existsSync()) {
File(path).deleteSync();
} }
setState(() { if (_recordingOffset.dx -
details.localPosition.dx >=
90 &&
_recordingState == RecordingState.recording) {
_recordingState = RecordingState.none; _recordingState = RecordingState.none;
HapticFeedback.heavyImpact();
_cancelAudioRecording();
}
setState(() {
final a = _recordingOffset.dx -
details.localPosition.dx;
if (a > 0 && a <= 90) {
_cancelSlideOffset = _recordingOffset.dx -
details.localPosition.dx;
}
}); });
}, },
onLongPressEnd: (a) => _stopAudioRecording(), onLongPressStart: (a) {
_recordingOffset = a.localPosition;
_startAudioRecording();
},
onLongPressCancel: _cancelAudioRecording,
onLongPressEnd: (a) {
if (_recordingState != RecordingState.recording) {
return;
}
if (!_audioRecordingLock) {
_stopAudioRecording();
}
},
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
if (_recordingState == RecordingState.recording) if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill(
top: -120,
left: -5,
child: Align(
alignment: AlignmentGeometry.topCenter,
child: Container(
padding: const EdgeInsets.only(top: 13),
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
color: Colors.black,
),
child: const Center(
child: Column(
children: [
FaIcon(
FontAwesomeIcons.lock,
size: 16,
),
SizedBox(height: 5),
FaIcon(
FontAwesomeIcons.angleUp,
size: 16,
),
],
),
),
),
),
),
if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill( Positioned.fill(
top: -20, top: -20,
left: -25, left: -25,
@ -275,6 +390,7 @@ class _MessageInputState extends State<MessageInput> {
height: 60, height: 60,
), ),
), ),
if (!_audioRecordingLock)
ColoredBox( ColoredBox(
color: Colors.transparent, color: Colors.transparent,
child: Padding( child: Padding(
@ -306,14 +422,15 @@ class _MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
if (_textFieldController.text != '') if (_textFieldController.text != '' || _audioRecordingLock)
IconButton( IconButton(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
icon: FaIcon( icon: FaIcon(
color: context.color.primary, color: context.color.primary,
FontAwesomeIcons.solidPaperPlane, FontAwesomeIcons.solidPaperPlane,
), ),
onPressed: _sendMessage, onPressed:
_audioRecordingLock ? _stopAudioRecording : _sendMessage,
) )
else else
IconButton( IconButton(

View file

@ -48,7 +48,7 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
Widget imageChild = Align( Widget imageChild = Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: SizedBox( child: SizedBox(
height: availableHeight, // height: availableHeight,
child: AspectRatio( child: AspectRatio(
aspectRatio: 9 / 16, aspectRatio: 9 / 16,
child: ClipRRect( child: ClipRRect(