mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:08:40 +00:00
fix #323
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
35a90ace0a
commit
a63343ccdd
8 changed files with 238 additions and 97 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,7 @@ class ChatAudioEntry extends StatelessWidget {
|
||||||
path: mediaService.tempPath.path,
|
path: mediaService.tempPath.path,
|
||||||
message: message,
|
message: message,
|
||||||
)
|
)
|
||||||
: (mediaService.originalPath.existsSync())
|
: Container()
|
||||||
? InChatAudioPlayer(
|
|
||||||
path: mediaService.originalPath.path,
|
|
||||||
message: message,
|
|
||||||
)
|
|
||||||
: 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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
recorderController: recorderController,
|
left: 12,
|
||||||
waveStyle: WaveStyle(
|
right: 8,
|
||||||
waveColor: isDarkMode(context)
|
),
|
||||||
? Colors.white
|
child: FaIcon(
|
||||||
: Colors.black,
|
FontAwesomeIcons.microphone,
|
||||||
extendWaveform: true,
|
size: 20,
|
||||||
showMiddleLine: false,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
const SizedBox(width: 10),
|
||||||
color: context.color.surfaceContainer,
|
Text(
|
||||||
),
|
formatMsToMinSec(
|
||||||
padding: const EdgeInsets.only(left: 18),
|
_currentDuration,
|
||||||
margin:
|
),
|
||||||
const EdgeInsets.symmetric(horizontal: 15),
|
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) {
|
setState(() {
|
||||||
await openAppSettings();
|
_audioRecordingLock = true;
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
if (!await Permission.microphone.isGranted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setState(() {
|
if (_recordingOffset.dx -
|
||||||
_recordingState = RecordingState.recording;
|
details.localPosition.dx >=
|
||||||
});
|
90 &&
|
||||||
await HapticFeedback.heavyImpact();
|
_recordingState == RecordingState.recording) {
|
||||||
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(() {
|
|
||||||
_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,30 +390,31 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
height: 60,
|
height: 60,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ColoredBox(
|
if (!_audioRecordingLock)
|
||||||
color: Colors.transparent,
|
ColoredBox(
|
||||||
child: Padding(
|
color: Colors.transparent,
|
||||||
padding: const EdgeInsets.only(
|
child: Padding(
|
||||||
top: 8,
|
padding: const EdgeInsets.only(
|
||||||
bottom: 8,
|
top: 8,
|
||||||
left: 8,
|
bottom: 8,
|
||||||
right: 12,
|
left: 8,
|
||||||
),
|
right: 12,
|
||||||
child: FaIcon(
|
),
|
||||||
size: 20,
|
child: FaIcon(
|
||||||
color: (_recordingState ==
|
size: 20,
|
||||||
RecordingState.recording)
|
color: (_recordingState ==
|
||||||
? Colors.white
|
RecordingState.recording)
|
||||||
: null,
|
? Colors.white
|
||||||
(_recordingState == RecordingState.none)
|
: null,
|
||||||
? FontAwesomeIcons.microphone
|
(_recordingState == RecordingState.none)
|
||||||
: (_recordingState ==
|
? FontAwesomeIcons.microphone
|
||||||
RecordingState.recording)
|
: (_recordingState ==
|
||||||
? FontAwesomeIcons.stop
|
RecordingState.recording)
|
||||||
: FontAwesomeIcons.play,
|
? FontAwesomeIcons.stop
|
||||||
|
: FontAwesomeIcons.play,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue